diff --git a/abtest-sample/pom.xml b/abtest-sample/pom.xml index 10d6174964dd050c79a236a2d0ac184767f51b42..13b7cd41ef5a522f6894e1de2420d1e3921c6476 100644 --- a/abtest-sample/pom.xml +++ b/abtest-sample/pom.xml @@ -14,7 +14,7 @@ com.dianping.cat - cat-core + cat-client junit diff --git a/abtest-sample/src/main/java/com/dianping/abtest/sample/ABTestSampleServlet.java b/abtest-sample/src/main/java/com/dianping/abtest/sample/ABTestSampleServlet.java index 7277384518507d60b9801e3018e5947bee72f9fe..a75644b2af84cad71d8c65397b9a06c0d5ed3378 100644 --- a/abtest-sample/src/main/java/com/dianping/abtest/sample/ABTestSampleServlet.java +++ b/abtest-sample/src/main/java/com/dianping/abtest/sample/ABTestSampleServlet.java @@ -34,7 +34,7 @@ public class ABTestSampleServlet extends HttpServlet { } public static enum MyABTestId implements ABTestName { - CASE1("demo1"); + CASE1("SampleTest"); private String m_id; diff --git a/abtest-sample/src/main/java/com/dianping/abtest/sample/IPDistributionStrategy.java b/abtest-sample/src/main/java/com/dianping/abtest/sample/IPDistributionStrategy.java index 9f145a2d5326d799676b2539765a059007cd0d45..20779aadced5f08548acb0f33f8e3d4f98ab63c8 100644 --- a/abtest-sample/src/main/java/com/dianping/abtest/sample/IPDistributionStrategy.java +++ b/abtest-sample/src/main/java/com/dianping/abtest/sample/IPDistributionStrategy.java @@ -5,40 +5,37 @@ import java.util.List; import javax.servlet.http.HttpServletRequest; import org.unidal.helper.Splitters; +import org.unidal.lookup.annotation.Inject; import org.unidal.lookup.util.StringUtils; import com.dianping.cat.abtest.spi.ABTestContext; -import com.dianping.cat.abtest.spi.ABTestEntity; import com.dianping.cat.abtest.spi.ABTestGroupStrategy; public class IPDistributionStrategy implements ABTestGroupStrategy { public static final String ID = "ip-distribution"; - public IPDistributionStrategy(){ + public IPDistributionStrategy() { System.out.println("new " + ID + " created"); } - + + @Inject("IP") + private String m_ipAddress; + + private List m_ips; + @Override public void apply(ABTestContext ctx) { - ABTestEntity entity = ctx.getEntity(); - String config = entity.getGroupStrategyConfiguration(); - List ips = Splitters.by(',').trim().noEmptyItem().split(config); HttpServletRequest req = ctx.getHttpServletRequest(); String address = getRemoteAddr(req); - String group = ctx.getCookielet("ab"); - - if(group != null && group.equals("A")){ - ctx.setGroupName("A"); - }else{ - for (String ip : ips) { - if (ip.equals(address)) { - ctx.setGroupName("A"); - ctx.setCookielet("ab", "A"); - ctx.setCookielet("hit", "1"); - return; - } + + for (String ip : m_ips) { + if (ip.equals(address)) { + ctx.setGroupName("A"); + return; } } + + ctx.setGroupName("B"); } public String getRemoteAddr(HttpServletRequest req) { @@ -58,7 +55,7 @@ public class IPDistributionStrategy implements ABTestGroupStrategy { } if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) { ip = req.getRemoteAddr(); - if(ip.equals("127.0.0.1") || ip.startsWith("0:0:0:0:0:0:0:1")){ + if (ip.equals("127.0.0.1") || ip.startsWith("0:0:0:0:0:0:0:1")) { ip = IPUtils.getFirstNoLoopbackIP4Address(); } } @@ -67,6 +64,7 @@ public class IPDistributionStrategy implements ABTestGroupStrategy { } @Override - public void init(ABTestEntity entity) { - } + public void init() { + m_ips = Splitters.by(',').trim().noEmptyItem().split(m_ipAddress); + } } diff --git a/abtest-sample/src/main/resources/META-INF/cat/client.xml b/abtest-sample/src/main/resources/META-INF/cat/client.xml index 18f0324bdd5bfd2a509488fd4a060a470e5c2253..a793f587697b3bc7845b640b55efa458015f6ce6 100755 --- a/abtest-sample/src/main/resources/META-INF/cat/client.xml +++ b/abtest-sample/src/main/resources/META-INF/cat/client.xml @@ -1,3 +1,3 @@ - + diff --git a/cat-client/pom.xml b/cat-client/pom.xml index d6a8c5d50a387b1946428e77105522963a6cd389..d48428467d7490f7382293b7b195cb0a5ddb8f8a 100644 --- a/cat-client/pom.xml +++ b/cat-client/pom.xml @@ -16,6 +16,11 @@ foundation-service 2.0.3 + + org.unidal.framework + java-fragment + 0.1.0 + org.jboss.netty netty @@ -55,8 +60,7 @@ ${basedir}/src/main/resources/META-INF/dal/model/client-manifest.xml, ${basedir}/src/main/resources/META-INF/dal/model/status-manifest.xml, - ${basedir}/src/main/resources/META-INF/dal/model/abtest-manifest.xml, - + ${basedir}/src/main/resources/META-INF/dal/model/abtest-manifest.xml, @@ -69,6 +73,16 @@ com.dianping.cat.build.ComponentsConfigurator + + generate dal model files + generate-sources + + dal-model + + + + + diff --git a/cat-client/src/main/java/com/dianping/cat/abtest/repository/ABTestEntityRepository.java b/cat-client/src/main/java/com/dianping/cat/abtest/repository/ABTestEntityRepository.java index f08ba01491a629cf1709b5d9cd09fd88e0cf73d6..05f49e70732b965a5cd9445ecc847abd5a2cd6d7 100644 --- a/cat-client/src/main/java/com/dianping/cat/abtest/repository/ABTestEntityRepository.java +++ b/cat-client/src/main/java/com/dianping/cat/abtest/repository/ABTestEntityRepository.java @@ -3,6 +3,8 @@ package com.dianping.cat.abtest.repository; import java.util.Map; import java.util.Set; +import javax.script.Invocable; + import com.dianping.cat.abtest.spi.ABTestEntity; public interface ABTestEntityRepository { @@ -10,5 +12,7 @@ public interface ABTestEntityRepository { public Set getActiveRuns(); + public Invocable getInvocable(int runID); + public String getAbtestModel(); } diff --git a/cat-client/src/main/java/com/dianping/cat/abtest/repository/FieldInjectUtil.java b/cat-client/src/main/java/com/dianping/cat/abtest/repository/FieldInjectUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..0b782e1071835b68bfd8d5abea67b661d11a3847 --- /dev/null +++ b/cat-client/src/main/java/com/dianping/cat/abtest/repository/FieldInjectUtil.java @@ -0,0 +1,33 @@ +package com.dianping.cat.abtest.repository; + +import com.dianping.cat.abtest.model.entity.Field; +import com.dianping.cat.abtest.model.entity.GroupstrategyDescriptor; +import com.dianping.cat.abtest.spi.ABTestGroupStrategy; + +public class FieldInjectUtil { + + public void inject(ABTestGroupStrategy targetGroupStrategy, GroupstrategyDescriptor descriptor) throws Exception { + for (Field field : descriptor.getFields()) { + java.lang.reflect.Field modifiersField = targetGroupStrategy.getClass().getDeclaredField( + field.getModifierName()); + + modifiersField.setAccessible(true); + + if (field.getType().equals("String")) { + modifiersField.set(targetGroupStrategy, field.getValue()); + } else if (field.getType().equals("int") || field.getType().equals("Integer")) { + modifiersField.setInt(targetGroupStrategy, Integer.parseInt(field.getValue())); + } else if (field.getType().equals("boolean") || field.getType().equals("Boolean")) { + modifiersField.setBoolean(targetGroupStrategy, Boolean.parseBoolean(field.getValue())); + } else if (field.getType().equals("long") || field.getType().equals("Long")) { + modifiersField.setLong(targetGroupStrategy, Long.parseLong(field.getValue())); + } else if (field.getType().equals("double") || field.getType().equals("Double")) { + modifiersField.setDouble(targetGroupStrategy, Double.parseDouble(field.getValue())); + } else if (field.getType().equals("float") || field.getType().equals("Float")) { + modifiersField.setFloat(targetGroupStrategy, Float.parseFloat(field.getValue())); + } else { + modifiersField.set(targetGroupStrategy, field.getValue()); + } + } + } +} diff --git a/cat-client/src/main/java/com/dianping/cat/abtest/repository/HttpABTestEntityRepository.java b/cat-client/src/main/java/com/dianping/cat/abtest/repository/HttpABTestEntityRepository.java index 27a439919229ca71d5b8a8f99b63ba5e19aa6ae5..a36d26697864cbcec5681e1031dcc0047f3c7f15 100644 --- a/cat-client/src/main/java/com/dianping/cat/abtest/repository/HttpABTestEntityRepository.java +++ b/cat-client/src/main/java/com/dianping/cat/abtest/repository/HttpABTestEntityRepository.java @@ -1,12 +1,16 @@ package com.dianping.cat.abtest.repository; import java.io.InputStream; -import java.util.HashMap; -import java.util.HashSet; +import java.util.Collections; import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.LockSupport; +import javax.script.Invocable; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; + import org.codehaus.plexus.personality.plexus.lifecycle.phase.Initializable; import org.codehaus.plexus.personality.plexus.lifecycle.phase.InitializationException; import org.unidal.helper.Files; @@ -32,18 +36,28 @@ public class HttpABTestEntityRepository extends ContainerHolder implements ABTes @Inject private ClientConfigManager m_configManager; - private String m_domain; + @Inject + private int m_refreshTimeInSeconds = 60; // seconds - private Map m_entities = new HashMap(); + private Map m_entities = new ConcurrentHashMap(); - private Set m_activeRuns = new HashSet(); + private Set m_activeRuns = Collections.newSetFromMap(new ConcurrentHashMap()); - @Inject - private int m_refreshTimeInSeconds = 60; // seconds + private Map m_strategies = new ConcurrentHashMap(); - private String m_abtestModel; + private Map m_invokeMap = new ConcurrentHashMap(); + + private FieldInjectUtil m_fieldInjector = new FieldInjectUtil(); + + private ScriptEngineManager m_mgr; - private Map m_strategies = new HashMap(); + private ScriptEngine m_engine; + + private String m_domain; + + private long m_lastUpdateTime = -1; + + private String m_abtestModel; @Override public Map getCurrentEntities() { @@ -55,34 +69,42 @@ public class HttpABTestEntityRepository extends ContainerHolder implements ABTes return getClass().getSimpleName(); } + public Invocable getInvocable(int runID) { + return m_invokeMap.get(runID); + } + @Override public void initialize() throws InitializationException { m_domain = m_configManager.getDomain().getId(); - + m_mgr = new ScriptEngineManager(); + m_engine = m_mgr.getEngineByExtension("java"); } private void refresh() { m_abtestModel = null; - + for (Server server : m_configManager.getServers()) { String ip = server.getIp(); int port = server.getHttpPort(); - String url = String.format("http://%s:%s/cat/s/abtest?op=model", ip, port); + String url = String.format("http://%s:%s/cat/s/abtest?op=model&lastUpdateTime=%s", ip, port, m_lastUpdateTime); Transaction t = Cat.newTransaction("ABTest", url); try { InputStream inputStream = Urls.forIO().connectTimeout(300).readTimeout(2000).openStream(url); String content = Files.forIO().readFrom(inputStream, "utf-8"); AbtestModel abtest = DefaultSaxParser.parse(content); - ABTestVisitor visitor = new ABTestVisitor(m_domain); - abtest.accept(visitor); + if (abtest.getCases() != null && abtest.getCases().size() > 0) { + ABTestVisitor visitor = new ABTestVisitor(m_domain); - // switch the entities - m_entities = visitor.getEntities(); - m_activeRuns = visitor.getActiveRuns(); - m_abtestModel = abtest.toString(); - break; + abtest.accept(visitor); + + // switch the entities + m_entities = visitor.getEntities(); + m_activeRuns = visitor.getActiveRuns(); + m_abtestModel = abtest.toString(); + break; + } } catch (Throwable e) { t.setStatus(e); Cat.logError(e); @@ -124,8 +146,6 @@ public class HttpABTestEntityRepository extends ContainerHolder implements ABTes public ABTestVisitor(String domain) { m_domain = domain; - m_entities = new HashMap(); - m_activeRuns = new HashSet(); } public Map getEntities() { @@ -138,44 +158,62 @@ public class HttpABTestEntityRepository extends ContainerHolder implements ABTes private void prepareEntity(Case _case, Run run) { ABTestEntity entity = new ABTestEntity(_case, run); - String strategyKey = String.format("%s:%s:%s", _case.getId(), entity.getGroupStrategyName(), - entity.getGroupStrategyConfiguration()); - ABTestGroupStrategy strategy = m_strategies.get(strategyKey); - - if (strategy != null) { - entity.setGroupStrategy(strategy); - } else { - try { - strategy = lookup(ABTestGroupStrategy.class, entity.getGroupStrategyName()); - strategy.init(entity); + + try { + if (m_strategies.get(run.getId()) != null && m_lastUpdateTime >= run.getLastModifiedDate().getTime()) { + entity.setGroupStrategy(m_strategies.get(run.getId())); + } else { + ABTestGroupStrategy strategy = lookup(ABTestGroupStrategy.class, entity.getGroupStrategyName()); + + m_fieldInjector.inject(strategy, run.getGroupstrategyDescriptor()); + strategy.init(); entity.setGroupStrategy(strategy); + m_strategies.put(run.getId(), strategy); + } - m_strategies.put(strategyKey, strategy); - } catch (Exception e) { - Cat.logError(e); + if (m_invokeMap.get(run.getId()) != null && m_lastUpdateTime >= run.getLastModifiedDate().getTime()) { + entity.setInvocable(m_invokeMap.get(run.getId())); + } else { + String javaFragement = run.getConditionsFragement(); - ABTestEntity origin = HttpABTestEntityRepository.this.m_entities.get(_case.getId()); + Invocable inv = (Invocable) m_engine.eval(javaFragement); + entity.setInvocable(inv); + m_invokeMap.put(run.getId(), inv); + } - if (origin != null) { - entity = origin; - } else { - entity.setDisabled(true); - } + } catch (Throwable e) { + Cat.logError(e); + ABTestEntity origin = m_entities.get(_case.getName()); + + if (origin != null) { + entity = origin; + } else { + entity.setDisabled(true); } + } finally { + m_entities.put(entity.getName(), entity); } - - m_entities.put(entity.getName(), entity); } @Override public void visitCase(Case _case) { + long maxUpdateTime = -1; + for (Run run : _case.getRuns()) { m_activeRuns.add(String.valueOf(run.getId())); if (run.getDomains() != null && run.getDomains().contains(m_domain)) { prepareEntity(_case, run); + + if (run.getLastModifiedDate().getTime() > maxUpdateTime) { + maxUpdateTime = run.getLastModifiedDate().getTime(); + } } } + + if (maxUpdateTime > m_lastUpdateTime) { + m_lastUpdateTime = maxUpdateTime; + } } } diff --git a/cat-client/src/main/java/com/dianping/cat/abtest/spi/ABTestContext.java b/cat-client/src/main/java/com/dianping/cat/abtest/spi/ABTestContext.java index 4f3ede2fd4c4ff75982089d361ff09aca442774f..433e5ce2445812fb29e5a8174e96e5fdf238fca7 100644 --- a/cat-client/src/main/java/com/dianping/cat/abtest/spi/ABTestContext.java +++ b/cat-client/src/main/java/com/dianping/cat/abtest/spi/ABTestContext.java @@ -4,7 +4,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public interface ABTestContext { - public final String DEFAULT_GROUP = ""; + public final String DEFAULT_GROUP = "Control"; public ABTestEntity getEntity(); diff --git a/cat-client/src/main/java/com/dianping/cat/abtest/spi/ABTestEntity.java b/cat-client/src/main/java/com/dianping/cat/abtest/spi/ABTestEntity.java index b3680c38869e1aa5d1fe6d6bcaa4f9e0fcebd41f..880771b86f395e8caff5110a98f57161c0302bad 100644 --- a/cat-client/src/main/java/com/dianping/cat/abtest/spi/ABTestEntity.java +++ b/cat-client/src/main/java/com/dianping/cat/abtest/spi/ABTestEntity.java @@ -1,9 +1,17 @@ package com.dianping.cat.abtest.spi; import java.util.Date; +import java.util.List; + +import javax.script.Invocable; import com.dianping.cat.abtest.model.entity.Case; +import com.dianping.cat.abtest.model.entity.Condition; +import com.dianping.cat.abtest.model.entity.ConversionRule; +import com.dianping.cat.abtest.model.entity.GroupstrategyDescriptor; import com.dianping.cat.abtest.model.entity.Run; +import com.dianping.cat.abtest.spi.internal.ABTestCodec; +import com.dianping.cat.message.spi.MessageManager; public class ABTestEntity { @@ -12,6 +20,12 @@ public class ABTestEntity { private Run m_run; private ABTestGroupStrategy m_groupStrategy; + + private Invocable m_invocable; + + private MessageManager m_messageManager; + + private ABTestCodec m_cookieCodec; private String m_groupStrategyName; @@ -27,35 +41,51 @@ public class ABTestEntity { } @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - ABTestEntity other = (ABTestEntity) obj; - if (m_groupStrategy == null) { - if (other.m_groupStrategy != null) - return false; - } else if (!m_groupStrategy.equals(other.m_groupStrategy)) - return false; - if (m_groupStrategyName == null) { - if (other.m_groupStrategyName != null) - return false; - } else if (!m_groupStrategyName.equals(other.m_groupStrategyName)) - return false; - if (m_name == null) { - if (other.m_name != null) - return false; - } else if (!m_name.equals(other.m_name)) - return false; - if (m_run == null) { - if (other.m_run != null) - return false; - } else if (!m_run.equals(other.m_run)) - return false; - return true; + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + ABTestEntity other = (ABTestEntity) obj; + if (m_groupStrategy == null) { + if (other.m_groupStrategy != null) + return false; + } else if (!m_groupStrategy.equals(other.m_groupStrategy)) + return false; + if (m_groupStrategyName == null) { + if (other.m_groupStrategyName != null) + return false; + } else if (!m_groupStrategyName.equals(other.m_groupStrategyName)) + return false; + if (m_name == null) { + if (other.m_name != null) + return false; + } else if (!m_name.equals(other.m_name)) + return false; + if (m_run == null) { + if (other.m_run != null) + return false; + } else if (!m_run.equals(other.m_run)) + return false; + return true; + } + + public List getConditions() { + return m_run.getConditions() != null ? m_run.getConditions() : null; + } + + public String getConditionsFragement() { + return m_run.getConditionsFragement(); + } + + public List getConversionRules() { + return m_run.getConversionRules(); + } + + public ABTestCodec getCookieCodec() { + return m_cookieCodec; } public Date getEndDate() { @@ -66,14 +96,22 @@ public class ABTestEntity { return m_groupStrategy; } - public String getGroupStrategyConfiguration() { - return m_run.getGroupStrategyConfiguration() != null ? m_run.getGroupStrategyConfiguration() : null; + public GroupstrategyDescriptor getGroupStrategyDescriptor() { + return m_run.getGroupstrategyDescriptor() != null ? m_run.getGroupstrategyDescriptor() : null; } public String getGroupStrategyName() { return m_groupStrategyName != null ? m_groupStrategyName : null; } + public Invocable getInvocable() { + return m_invocable; + } + + public MessageManager getMessageManager() { + return m_messageManager; + } + public String getName() { return m_name; } @@ -87,20 +125,20 @@ public class ABTestEntity { } @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((m_groupStrategy == null) ? 0 : m_groupStrategy.hashCode()); - result = prime * result + ((m_groupStrategyName == null) ? 0 : m_groupStrategyName.hashCode()); - result = prime * result + ((m_name == null) ? 0 : m_name.hashCode()); - result = prime * result + ((m_run == null) ? 0 : m_run.hashCode()); - return result; - } - + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((m_groupStrategy == null) ? 0 : m_groupStrategy.hashCode()); + result = prime * result + ((m_groupStrategyName == null) ? 0 : m_groupStrategyName.hashCode()); + result = prime * result + ((m_name == null) ? 0 : m_name.hashCode()); + result = prime * result + ((m_run == null) ? 0 : m_run.hashCode()); + return result; + } + public boolean isDisabled() { return m_run.isDisabled(); } - + public boolean isEligible(Date date) { if (m_run.isDisabled()) { return false; @@ -123,6 +161,10 @@ public class ABTestEntity { return true; } + public void setCookieCodec(ABTestCodec cookieCodec) { + m_cookieCodec = cookieCodec; + } + public void setDisabled(boolean disabled) { m_run.setDisabled(disabled); } @@ -131,13 +173,21 @@ public class ABTestEntity { m_groupStrategy = groupStrategy; } - public void setName(String name) { - m_name = name; + public void setInvocable(Invocable invocable) { + m_invocable = invocable; + } + + public void setMessageManager(MessageManager messageManager) { + m_messageManager = messageManager; } + public void setName(String name) { + m_name = name; + } + @Override - public String toString() { - return "ABTestEntity [m_name=" + m_name + ", m_groupStrategyName=" + m_groupStrategyName + ", m_run=" + m_run - + ", m_groupStrategy=" + m_groupStrategy + "]"; - } + public String toString() { + return "ABTestEntity [m_name=" + m_name + ", m_groupStrategyName=" + m_groupStrategyName + ", m_run=" + m_run + + ", m_groupStrategy=" + m_groupStrategy + "]"; + } } \ No newline at end of file diff --git a/cat-client/src/main/java/com/dianping/cat/abtest/spi/ABTestGroupStrategy.java b/cat-client/src/main/java/com/dianping/cat/abtest/spi/ABTestGroupStrategy.java index 18095852d9276ac566fca2d06f19bd65e61a7c7a..ae24c581f5df177e6d0377b8638d26635b64d9bf 100644 --- a/cat-client/src/main/java/com/dianping/cat/abtest/spi/ABTestGroupStrategy.java +++ b/cat-client/src/main/java/com/dianping/cat/abtest/spi/ABTestGroupStrategy.java @@ -3,5 +3,5 @@ package com.dianping.cat.abtest.spi; public interface ABTestGroupStrategy { public void apply(ABTestContext ctx); - public void init(ABTestEntity entity); + public void init(); } \ No newline at end of file diff --git a/cat-client/src/main/java/com/dianping/cat/abtest/spi/internal/ABTestCodec.java b/cat-client/src/main/java/com/dianping/cat/abtest/spi/internal/ABTestCodec.java index 8bb2392db46b514991e25e9dbb5a0746e385d07c..73c8286fd25b7d4555ce324bb2a49d96805ea04d 100644 --- a/cat-client/src/main/java/com/dianping/cat/abtest/spi/internal/ABTestCodec.java +++ b/cat-client/src/main/java/com/dianping/cat/abtest/spi/internal/ABTestCodec.java @@ -5,8 +5,11 @@ import java.util.Set; public interface ABTestCodec { public String encode(Map> map); + + public String encode(String runId, Map map); public Map decode(String value); public Map> decode(String value, Set keys); + } diff --git a/cat-client/src/main/java/com/dianping/cat/abtest/spi/internal/DefaultABTestCodec.java b/cat-client/src/main/java/com/dianping/cat/abtest/spi/internal/DefaultABTestCodec.java index 8f8ae72a6f43d13386da5416344fbe8b43b54ea4..0a80e5b5e2a731245341e51c311b3927ce5da5bc 100644 --- a/cat-client/src/main/java/com/dianping/cat/abtest/spi/internal/DefaultABTestCodec.java +++ b/cat-client/src/main/java/com/dianping/cat/abtest/spi/internal/DefaultABTestCodec.java @@ -12,13 +12,18 @@ public class DefaultABTestCodec implements ABTestCodec { @Override protected boolean removeEldestEntry(Entry> arg0) { - return true; + return size() >= 1000; } }; public Map> decode(String value, Set keys) { int len = value.length(); Map> map = new LinkedHashMap>(); + + if (len == 0) { + return map; + } + StringBuilder key = new StringBuilder(); StringBuilder name = new StringBuilder(); StringBuilder val = new StringBuilder(); @@ -120,25 +125,41 @@ public class DefaultABTestCodec implements ABTestCodec { sb.append('&'); } - boolean first2 = true; + String part = encode(runId, cookielets); + + sb.append(part); + } + + return sb.toString(); + } + + @Override + public String encode(String runId, Map map) { + StringBuilder sb; + if (map != null && map.size() > 0) { + sb = new StringBuilder(32); sb.append(runId).append('='); + } else { + return ""; + } - for (Map.Entry e : cookielets.entrySet()) { - if (first2) { - first2 = false; - } else { - sb.append('|'); - } + boolean first = true; - String key = e.getKey(); - String value = e.getValue(); - - if(value == null){ - sb.append(key).append(':'); - }else{ - sb.append(key).append(':').append(value); - } + for (Map.Entry e : map.entrySet()) { + if (first) { + first = false; + } else { + sb.append('|'); + } + + String key = e.getKey(); + String value = e.getValue(); + + if (value == null) { + sb.append(key).append(':'); + } else { + sb.append(key).append(':').append(value); } } @@ -156,7 +177,7 @@ public class DefaultABTestCodec implements ABTestCodec { for (Entry> entry : maps.entrySet()) { String key = entry.getKey(); Map entryValue = entry.getValue(); - + if (entryValue != null) { String val = entryValue.get("ab"); diff --git a/cat-client/src/main/java/com/dianping/cat/abtest/spi/internal/DefaultABTestContext.java b/cat-client/src/main/java/com/dianping/cat/abtest/spi/internal/DefaultABTestContext.java index 9ad3045ef1c37adf8767ddf202f72f0bde83d2ac..49e6b90ecfd56e80fbb74f84b1dd4d4b26b91576 100644 --- a/cat-client/src/main/java/com/dianping/cat/abtest/spi/internal/DefaultABTestContext.java +++ b/cat-client/src/main/java/com/dianping/cat/abtest/spi/internal/DefaultABTestContext.java @@ -2,17 +2,21 @@ package com.dianping.cat.abtest.spi.internal; import java.util.Date; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; +import javax.script.Invocable; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.dianping.cat.Cat; +import com.dianping.cat.abtest.model.entity.ConversionRule; import com.dianping.cat.abtest.spi.ABTestContext; import com.dianping.cat.abtest.spi.ABTestEntity; import com.dianping.cat.abtest.spi.ABTestGroupStrategy; import com.dianping.cat.message.Message; import com.dianping.cat.message.Transaction; +import com.dianping.cat.message.internal.DefaultMessageManager; public class DefaultABTestContext implements ABTestContext { private String m_groupName = DEFAULT_GROUP; @@ -80,24 +84,35 @@ public class DefaultABTestContext implements ABTestContext { @Override public void setGroupName(String groupName) { m_groupName = groupName; + setCookielet("ab", groupName); } public void setGroupStrategy(ABTestGroupStrategy groupStrategy) { m_groupStrategy = groupStrategy; } + public void setCookielets(Map cookielets) { + m_cookielets = cookielets; + } + public void setup(HttpServletRequest request, HttpServletResponse response, Map cookielets) { m_request = request; m_response = response; m_cookielets = cookielets; - if (m_entity.isEligible(new Date())) { + Invocable inv = m_entity.getInvocable(); + + if (inv != null && m_entity.isEligible(new Date())) { + boolean isAccept = false; Transaction t = Cat.newTransaction("GroupStrategy", m_entity.getGroupStrategyName()); try { - m_groupStrategy.apply(this); + isAccept = (Boolean) inv.invokeFunction("isEligible", request); - t.setStatus(Message.SUCCESS); + if (isAccept) { + m_groupStrategy.apply(this); + t.setStatus(Message.SUCCESS); + } } catch (Throwable e) { t.setStatus(e); Cat.logError(e); @@ -105,5 +120,31 @@ public class DefaultABTestContext implements ABTestContext { t.complete(); } } + + String actual = (String) request.getAttribute("url-rewrite-original-url"); + List conversionRules = m_entity.getConversionRules(); + + if (conversionRules != null) { + for (ConversionRule rule : conversionRules) { + if (actual.equalsIgnoreCase(rule.getText())) { + String key = String.valueOf(m_entity.getRun().getId()); + String appendMetricType = m_entity.getCookieCodec().encode(key, m_cookielets); + + if (appendMetricType != null && appendMetricType.length() > 0) { + // DefaultMessageManager manager = (DefaultMessageManager) Cat.getManager(); + DefaultMessageManager defaultMessageManager = (DefaultMessageManager) m_entity.getMessageManager(); + String metricType = defaultMessageManager.getMetricType(); + + if (metricType != null && metricType.length() > 0) { + defaultMessageManager.setMetricType(metricType + "&" + appendMetricType); + } else { + defaultMessageManager.setMetricType(appendMetricType); + } + } + + break; + } + } + } } } diff --git a/cat-client/src/main/java/com/dianping/cat/abtest/spi/internal/DefaultABTestContextManager.java b/cat-client/src/main/java/com/dianping/cat/abtest/spi/internal/DefaultABTestContextManager.java index fc211bd5cf735340192f753e51e114fb892b38a4..fcc2b2824453aa91743703fa9a9f127f1aa06078 100644 --- a/cat-client/src/main/java/com/dianping/cat/abtest/spi/internal/DefaultABTestContextManager.java +++ b/cat-client/src/main/java/com/dianping/cat/abtest/spi/internal/DefaultABTestContextManager.java @@ -16,8 +16,6 @@ import com.dianping.cat.abtest.ABTestName; import com.dianping.cat.abtest.spi.ABTestContext; import com.dianping.cat.abtest.spi.ABTestEntity; import com.dianping.cat.abtest.spi.ABTestGroupStrategy; -import com.dianping.cat.message.internal.DefaultMessageManager; -import com.dianping.cat.message.spi.MessageManager; public class DefaultABTestContextManager extends ContainerHolder implements ABTestContextManager { private static final String ABTEST_COOKIE_NAME = "ab"; @@ -25,9 +23,6 @@ public class DefaultABTestContextManager extends ContainerHolder implements ABTe @Inject private ABTestEntityManager m_entityManager; - @Inject - private MessageManager m_messageManager; - @Inject private ABTestCodec m_cookieCodec; @@ -49,8 +44,11 @@ public class DefaultABTestContextManager extends ContainerHolder implements ABTe @Override public void onRequestBegin(HttpServletRequest request, HttpServletResponse response) { Entry entry = m_threadLocal.get(); + String requestUrl = (String) request.getAttribute("url-rewrite-original-url"); - entry.setup(request, response); + if (requestUrl == null || !requestUrl.contains("ajax")) { + entry.setup(request, response); + } } @Override @@ -145,10 +143,6 @@ public class DefaultABTestContextManager extends ContainerHolder implements ABTe String newValue = m_cookieCodec.encode(map); setCookie(request, response, ABTEST_COOKIE_NAME, newValue); - - if (newValue != null && newValue.length() > 0) { - ((DefaultMessageManager) m_messageManager).setMetricType(newValue); - } } } } diff --git a/cat-client/src/main/java/com/dianping/cat/abtest/spi/internal/DefaultABTestEntityManager.java b/cat-client/src/main/java/com/dianping/cat/abtest/spi/internal/DefaultABTestEntityManager.java index f87c9c435f7efd1cf3f5cf42b7bbd1b42cd3ff2b..2bc08cafd4c41b2de61faa501bb4d6398f5d6474 100644 --- a/cat-client/src/main/java/com/dianping/cat/abtest/spi/internal/DefaultABTestEntityManager.java +++ b/cat-client/src/main/java/com/dianping/cat/abtest/spi/internal/DefaultABTestEntityManager.java @@ -13,13 +13,19 @@ import com.dianping.cat.Cat; import com.dianping.cat.abtest.ABTestName; import com.dianping.cat.abtest.repository.ABTestEntityRepository; import com.dianping.cat.abtest.spi.ABTestEntity; -import com.dianping.cat.abtest.spi.ABTestGroupStrategy; import com.dianping.cat.message.Message; +import com.dianping.cat.message.spi.MessageManager; public class DefaultABTestEntityManager extends ContainerHolder implements ABTestEntityManager, Initializable { @Inject private ABTestEntityRepository m_repository; + @Inject + private MessageManager m_messageManager; + + @Inject + private ABTestCodec m_cookieCodec; + @Override public ABTestEntity getEntity(ABTestName name) { String id = name.getValue(); @@ -42,27 +48,24 @@ public class DefaultABTestEntityManager extends ContainerHolder implements ABTes List entitiesList = new ArrayList(); for (ABTestEntity entity : m_repository.getCurrentEntities().values()) { + entity.setMessageManager(m_messageManager); + entity.setCookieCodec(m_cookieCodec); + entitiesList.add(entity); } return entitiesList; } - - public Set getActiveRun(){ + + public Set getActiveRun() { return m_repository.getActiveRuns(); } @Override public void initialize() throws InitializationException { for (ABTestEntity entity : m_repository.getCurrentEntities().values()) { - try { - ABTestGroupStrategy groupStrategy = lookup(ABTestGroupStrategy.class, entity.getGroupStrategyName()); - - entity.setGroupStrategy(groupStrategy); - } catch (Exception e) { - Cat.logError(e); - entity.setDisabled(true); - } + entity.setMessageManager(m_messageManager); + entity.setCookieCodec(m_cookieCodec); } } } \ No newline at end of file diff --git a/cat-client/src/main/java/com/dianping/cat/abtest/spi/internal/conditions/ABTestCondition.java b/cat-client/src/main/java/com/dianping/cat/abtest/spi/internal/conditions/ABTestCondition.java new file mode 100644 index 0000000000000000000000000000000000000000..bd495a2d4f56a9586baf9453b57519be2ff9bc31 --- /dev/null +++ b/cat-client/src/main/java/com/dianping/cat/abtest/spi/internal/conditions/ABTestCondition.java @@ -0,0 +1,7 @@ +package com.dianping.cat.abtest.spi.internal.conditions; + +import javax.servlet.http.HttpServletRequest; + +public interface ABTestCondition { + public boolean accept(HttpServletRequest request); +} diff --git a/cat-client/src/main/java/com/dianping/cat/abtest/spi/internal/groupstrategy/TrafficDistributionGroupStrategy.java b/cat-client/src/main/java/com/dianping/cat/abtest/spi/internal/groupstrategy/TrafficDistributionGroupStrategy.java new file mode 100644 index 0000000000000000000000000000000000000000..39a7e33925024ef0173efbf0e57814c9aeab15fe --- /dev/null +++ b/cat-client/src/main/java/com/dianping/cat/abtest/spi/internal/groupstrategy/TrafficDistributionGroupStrategy.java @@ -0,0 +1,45 @@ +package com.dianping.cat.abtest.spi.internal.groupstrategy; + +import org.unidal.lookup.annotation.Inject; + +import com.dianping.cat.abtest.spi.ABTestContext; +import com.dianping.cat.abtest.spi.ABTestGroupStrategy; + +public class TrafficDistributionGroupStrategy implements ABTestGroupStrategy { + + public static final String ID = "OneVariationStrategy"; + + @Inject("Control") + private int m_percentControl = 50; + + @Inject("Variation-A") + private int m_percentA = 50; + + private final int m_total = 100; + + private int m_scoreControl = 0; + + private int m_scoreA = 0; + + @Override + public void apply(ABTestContext ctx) { + m_scoreControl += m_percentControl; + m_scoreA += m_percentA; + + if (m_scoreA >= m_scoreControl) { + ctx.setGroupName("A"); + m_scoreA -= m_total; + } else { + ctx.setGroupName(ABTestContext.DEFAULT_GROUP); + m_scoreControl -= m_total; + } + } + + @Override + public void init() { + if ((m_percentA + m_percentControl) > 100) { + m_percentControl = 50; + m_percentA = 50; + } + } +} diff --git a/cat-client/src/main/java/com/dianping/cat/build/ABTestComponentConfigurator.java b/cat-client/src/main/java/com/dianping/cat/build/ABTestComponentConfigurator.java index 696317ab107fcd7c07055ac72c8fa4d9d95483a2..c62c008127627741d3475c923644a19820baa2a0 100644 --- a/cat-client/src/main/java/com/dianping/cat/build/ABTestComponentConfigurator.java +++ b/cat-client/src/main/java/com/dianping/cat/build/ABTestComponentConfigurator.java @@ -8,12 +8,14 @@ import org.unidal.lookup.configuration.Component; import com.dianping.cat.abtest.repository.ABTestEntityRepository; import com.dianping.cat.abtest.repository.HttpABTestEntityRepository; +import com.dianping.cat.abtest.spi.ABTestGroupStrategy; import com.dianping.cat.abtest.spi.internal.ABTestCodec; import com.dianping.cat.abtest.spi.internal.ABTestContextManager; import com.dianping.cat.abtest.spi.internal.ABTestEntityManager; import com.dianping.cat.abtest.spi.internal.DefaultABTestCodec; import com.dianping.cat.abtest.spi.internal.DefaultABTestContextManager; import com.dianping.cat.abtest.spi.internal.DefaultABTestEntityManager; +import com.dianping.cat.abtest.spi.internal.groupstrategy.TrafficDistributionGroupStrategy; import com.dianping.cat.configuration.ClientConfigManager; import com.dianping.cat.message.spi.MessageManager; @@ -23,14 +25,16 @@ public class ABTestComponentConfigurator extends AbstractResourceConfigurator { List all = new ArrayList(); all.add(C(ABTestContextManager.class, DefaultABTestContextManager.class) // - .req(ABTestEntityManager.class, MessageManager.class, ABTestCodec.class)); + .req(ABTestEntityManager.class, ABTestCodec.class)); all.add(C(ABTestCodec.class, DefaultABTestCodec.class)); all.add(C(ABTestEntityRepository.class, HttpABTestEntityRepository.class) // .req(ClientConfigManager.class).config(E("refreshTimeInSeconds").value("60"))); all.add(C(ABTestEntityManager.class, DefaultABTestEntityManager.class) // - .req(ABTestEntityRepository.class)); + .req(ABTestEntityRepository.class, MessageManager.class,ABTestCodec.class)); + + all.add(C(ABTestGroupStrategy.class, TrafficDistributionGroupStrategy.ID, TrafficDistributionGroupStrategy.class)); return all; } diff --git a/cat-client/src/main/java/com/dianping/cat/servlet/CatFilter.java b/cat-client/src/main/java/com/dianping/cat/servlet/CatFilter.java index 02825aba21aaad5f62b9672537d067d002bc41a7..4f2d64fe1c4ffd8bb6d8453a99105ca9603df2d2 100644 --- a/cat-client/src/main/java/com/dianping/cat/servlet/CatFilter.java +++ b/cat-client/src/main/java/com/dianping/cat/servlet/CatFilter.java @@ -37,7 +37,8 @@ public class CatFilter implements Filter { } @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, + ServletException { Context ctx = new Context((HttpServletRequest) request, (HttpServletResponse) response, chain, m_handlers); ctx.handle(); @@ -247,15 +248,27 @@ public class CatFilter implements Filter { @Override public void handle(Context ctx) throws IOException, ServletException { if (ctx.isTop()) { + + HttpServletRequest req = ctx.getRequest(); + HttpServletResponse res = ctx.getResponse(); + + ABTestManager.onRequestBegin(req, res); + DefaultMessageManager manager = (DefaultMessageManager) Cat.getManager(); String metricType = manager.getMetricType(); - + if (metricType != null && metricType.length() > 0) { Cat.logEvent(ctx.getType(), "ABTest", Message.SUCCESS, metricType); } + + try { + ctx.handle(); + } finally { + ABTestManager.onRequestEnd(); + } + } else { + ctx.handle(); } - - ctx.handle(); } }, diff --git a/cat-client/src/main/resources/META-INF/dal/model/abtest-codegen.xml b/cat-client/src/main/resources/META-INF/dal/model/abtest-codegen.xml index 741ec31f53c1cf4ad9a4ffe6bbe08c2b20d54097..068b7c32d38b5b1149076926a9528ea989817c17 100644 --- a/cat-client/src/main/resources/META-INF/dal/model/abtest-codegen.xml +++ b/cat-client/src/main/resources/META-INF/dal/model/abtest-codegen.xml @@ -18,10 +18,39 @@ + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cat-client/src/main/resources/META-INF/dal/model/abtest-model.xml b/cat-client/src/main/resources/META-INF/dal/model/abtest-model.xml index d4f7f0d7c2e14b16e2879ff8848ea2a673bcb266..405e317a235dd6f54d5b489923605acbf77e4add 100644 --- a/cat-client/src/main/resources/META-INF/dal/model/abtest-model.xml +++ b/cat-client/src/main/resources/META-INF/dal/model/abtest-model.xml @@ -1,5 +1,5 @@ - + @@ -8,5 +8,16 @@ + + + + + + + + + + + diff --git a/cat-client/src/main/resources/META-INF/plexus/components.xml b/cat-client/src/main/resources/META-INF/plexus/components.xml index ff9cd24cea40d51f1189ae8fbcf707783e43dc73..73126db1f4cbd835fb0f7e178dda94f6ef30300c 100644 --- a/cat-client/src/main/resources/META-INF/plexus/components.xml +++ b/cat-client/src/main/resources/META-INF/plexus/components.xml @@ -137,9 +137,6 @@ com.dianping.cat.abtest.spi.internal.ABTestEntityManager - - com.dianping.cat.message.spi.MessageManager - com.dianping.cat.abtest.spi.internal.ABTestCodec @@ -168,7 +165,18 @@ com.dianping.cat.abtest.repository.ABTestEntityRepository + + com.dianping.cat.message.spi.MessageManager + + + com.dianping.cat.abtest.spi.internal.ABTestCodec + + + com.dianping.cat.abtest.spi.ABTestGroupStrategy + OneVariationStrategy + com.dianping.cat.abtest.spi.internal.groupstrategy.TrafficDistributionGroupStrategy + diff --git a/cat-client/src/main/resources/META-INF/wizard/model/wizard.xml b/cat-client/src/main/resources/META-INF/wizard/model/wizard.xml index 715c06aa5d73940a3b2c43adb6ab88cc2e2583df..9bbe592974a10617b22a742c9c316d9bddebafc9 100644 --- a/cat-client/src/main/resources/META-INF/wizard/model/wizard.xml +++ b/cat-client/src/main/resources/META-INF/wizard/model/wizard.xml @@ -2,5 +2,5 @@ src/test/resources/com/dianping/cat/abtest/spi/internal/abtest.xml - + diff --git a/cat-client/src/main/resources/com/dianping/cat/status/model/status.xsd b/cat-client/src/main/resources/com/dianping/cat/status/model/status.xsd index da6bb88239256c3c52bf7fe1822f121a852135b7..f649c9c7c44ceddd70aeafa8206206c3b5a0db82 100644 --- a/cat-client/src/main/resources/com/dianping/cat/status/model/status.xsd +++ b/cat-client/src/main/resources/com/dianping/cat/status/model/status.xsd @@ -1,5 +1,4 @@ - - + diff --git a/cat-client/src/test/java/com/dianping/cat/abtest/demo/roundrobin/RoundRobinGroupStrategy.java b/cat-client/src/test/java/com/dianping/cat/abtest/demo/roundrobin/RoundRobinGroupStrategy.java index 02275b319809c97833269e6fbf88a5572343336c..487dc85a0336bf178ecf3f83802afda4872d6800 100644 --- a/cat-client/src/test/java/com/dianping/cat/abtest/demo/roundrobin/RoundRobinGroupStrategy.java +++ b/cat-client/src/test/java/com/dianping/cat/abtest/demo/roundrobin/RoundRobinGroupStrategy.java @@ -3,7 +3,6 @@ package com.dianping.cat.abtest.demo.roundrobin; import java.util.concurrent.atomic.AtomicInteger; import com.dianping.cat.abtest.spi.ABTestContext; -import com.dianping.cat.abtest.spi.ABTestEntity; import com.dianping.cat.abtest.spi.ABTestGroupStrategy; public class RoundRobinGroupStrategy implements ABTestGroupStrategy { @@ -23,6 +22,6 @@ public class RoundRobinGroupStrategy implements ABTestGroupStrategy { } @Override - public void init(ABTestEntity entity) { + public void init() { } } diff --git a/cat-client/src/test/java/com/dianping/cat/abtest/spi/internal/ABTestContextManagerTest.java b/cat-client/src/test/java/com/dianping/cat/abtest/spi/internal/ABTestContextManagerTest.java index 6afd9ff3ea5ecad037f7a55344445b6309d07555..4859a669446897f1e1fa671184e3e39cd2ef9297 100644 --- a/cat-client/src/test/java/com/dianping/cat/abtest/spi/internal/ABTestContextManagerTest.java +++ b/cat-client/src/test/java/com/dianping/cat/abtest/spi/internal/ABTestContextManagerTest.java @@ -15,6 +15,9 @@ public class ABTestContextManagerTest extends ComponentTestCase { check("1=ab:A|cd:B&2=ab:A|cd:B", "1=ab:A|cd:B&2=ab:A|cd:B"); check("1=ab:|cd:B&2=ab:A|cd:B", "1=ab:|cd:B&2=ab:A|cd:B"); check("1=ab:A|cd:B&2=ab:A|cd:", "1=ab:A|cd:B&2=ab:A|cd:"); + check("1=ab:|cd:B&2=ab:A|cd:", "1=ab:|cd:B&2=ab:A|cd:"); + check("1=ab:|cd:", "1=ab:|cd:"); + check("1=ab:|cd:A", "1=ab:|cd:A"); check("", ""); check("1=ab:A|cd:B&2=ab:A|cd:B", "1=ab:A|cd:B&2=ab:A|cd:B", "1", "2"); @@ -35,6 +38,9 @@ public class ABTestContextManagerTest extends ComponentTestCase { check2("1=ab:A&2=ab:A|cd:B", "{1=A, 2=A}"); check2("1=ab:A|cd:B&2=ab:|cd:B", "{1=A, 2=}"); check2("1=cd:B&2=ab:A|cd:B", "{2=A}"); + check2("30=l:A|ab:A", "{30=A}"); + check2("1=ab:|cd:", "{1=}"); + check2("1=ab:|cd:A", "{1=}"); check2("", "{}"); } @@ -44,4 +50,20 @@ public class ABTestContextManagerTest extends ComponentTestCase { Assert.assertEquals(expected, map.toString()); } + + @Test + public void testCodec3() throws Exception{ + check3("1", "1=ab:A"); + } + + private void check3(String runId, String source) throws Exception { + ABTestCodec codec = lookup(ABTestCodec.class); + Map> map = codec.decode(source,null); + + System.out.println(map.get(runId)); + + String actual = codec.encode(runId,map.get(runId)); + + Assert.assertEquals(source, actual); + } } diff --git a/cat-client/src/test/java/com/dianping/cat/abtest/spi/internal/ABTestEntityManagerTest.java b/cat-client/src/test/java/com/dianping/cat/abtest/spi/internal/ABTestEntityManagerTest.java index c2e74b3720df1bc8c22be74fcbeada294458f39f..8d51a5dcff623e2aac3751cb3993bd05353c661d 100644 --- a/cat-client/src/test/java/com/dianping/cat/abtest/spi/internal/ABTestEntityManagerTest.java +++ b/cat-client/src/test/java/com/dianping/cat/abtest/spi/internal/ABTestEntityManagerTest.java @@ -45,7 +45,7 @@ public class ABTestEntityManagerTest extends ComponentTestCase { } if (expectedGroupStrategy != null) { - Assert.assertEquals(expectedGroupStrategyConfiguration, entity.getGroupStrategyConfiguration()); + Assert.assertEquals(expectedGroupStrategyConfiguration, entity.getGroupStrategyDescriptor()); } Assert.assertEquals(expectedEntityName, entity.getName()); diff --git a/cat-client/src/test/java/com/dianping/cat/abtest/spi/internal/AbtestModelTest.java b/cat-client/src/test/java/com/dianping/cat/abtest/spi/internal/AbtestModelTest.java deleted file mode 100644 index c4367b98c9ada6089c117000b552ba6bf54379d7..0000000000000000000000000000000000000000 --- a/cat-client/src/test/java/com/dianping/cat/abtest/spi/internal/AbtestModelTest.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.dianping.cat.abtest.spi.internal; - -import java.io.IOException; -import java.io.InputStream; - -import org.junit.Test; -import org.xml.sax.SAXException; - -import com.dianping.cat.abtest.model.entity.AbtestModel; -import com.dianping.cat.abtest.model.transform.DefaultSaxParser; - -public class AbtestModelTest { - @Test - public void test() throws SAXException, IOException { - InputStream in = getClass().getResourceAsStream("abtest.xml"); - AbtestModel abtest = DefaultSaxParser.parse(in); - - System.out.println(abtest); - } -} diff --git a/cat-client/src/test/resources/com/dianping/cat/abtest/spi/internal/ABTestFilterManagerTest.xml b/cat-client/src/test/resources/com/dianping/cat/abtest/spi/internal/ABTestFilterManagerTest.xml new file mode 100644 index 0000000000000000000000000000000000000000..1c34f4cfb9c9d07aa75339086c3b83b0de277b5d --- /dev/null +++ b/cat-client/src/test/resources/com/dianping/cat/abtest/spi/internal/ABTestFilterManagerTest.xml @@ -0,0 +1,20 @@ + + + + com.dianping.cat.abtest.spi.interanl.conditions.ABTestCondition + true + com.dianping.cat.abtest.spi.internal.ABTestFilterManagerTest$TestCondition + + true + + + + com.dianping.cat.abtest.spi.interanl.conditions.ABTestCondition + false + com.dianping.cat.abtest.spi.internal.ABTestFilterManagerTest$TestCondition + + false + + + + diff --git a/cat-client/src/test/resources/com/dianping/cat/abtest/spi/internal/abtest.xml b/cat-client/src/test/resources/com/dianping/cat/abtest/spi/internal/abtest.xml index 658a55965e038806014a3cbf1be7a6ce8ab8c4d7..6bf00260c382f5ec0110c03328e8c6a941c10d9d 100644 --- a/cat-client/src/test/resources/com/dianping/cat/abtest/spi/internal/abtest.xml +++ b/cat-client/src/test/resources/com/dianping/cat/abtest/spi/internal/abtest.xml @@ -7,11 +7,33 @@ TuanGouWeb TuanGouRemote - + hao.zhu@dianping.com TuanGouWeb TuanGouRemote - + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cat-consumer-advanced/src/main/resources/META-INF/dal/model/abtest-report-codegen.xml b/cat-consumer-advanced/src/main/resources/META-INF/dal/model/abtest-report-codegen.xml new file mode 100644 index 0000000000000000000000000000000000000000..6c3cdd486b16cddb42982f14bdf3f3158d0fb838 --- /dev/null +++ b/cat-consumer-advanced/src/main/resources/META-INF/dal/model/abtest-report-codegen.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/cat-consumer-advanced/src/main/resources/META-INF/dal/model/abtest-report-manifest.xml b/cat-consumer-advanced/src/main/resources/META-INF/dal/model/abtest-report-manifest.xml new file mode 100644 index 0000000000000000000000000000000000000000..e703b09764b3f4ca4f60c6886b8ec0cbb392c644 --- /dev/null +++ b/cat-consumer-advanced/src/main/resources/META-INF/dal/model/abtest-report-manifest.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/cat-consumer-advanced/src/main/resources/META-INF/dal/model/abtest-report-model.xml b/cat-consumer-advanced/src/main/resources/META-INF/dal/model/abtest-report-model.xml new file mode 100644 index 0000000000000000000000000000000000000000..847bd5b7552dca9943cad35ce28eb81b2104b64a --- /dev/null +++ b/cat-consumer-advanced/src/main/resources/META-INF/dal/model/abtest-report-model.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/cat-core/src/main/java/com/dianping/cat/Constants.java b/cat-core/src/main/java/com/dianping/cat/Constants.java index b4aa7bbccfa3050d088bd6ab9e985af0e3702674..024b57f4518cc44f6b652e6ed73ccc7a5b916aeb 100644 --- a/cat-core/src/main/java/com/dianping/cat/Constants.java +++ b/cat-core/src/main/java/com/dianping/cat/Constants.java @@ -20,4 +20,6 @@ public class Constants { public static final String REPORT_UTILIZATION = "utilization"; public static final String REPORT_HEAVY = "heavy"; + + public static final String ABTEST = "abtest"; } diff --git a/cat-core/src/main/java/com/dianping/cat/abtest/mockit/GroupStrategyTester.java b/cat-core/src/main/java/com/dianping/cat/abtest/mockit/GroupStrategyTester.java new file mode 100644 index 0000000000000000000000000000000000000000..e085769d65cf10a1ebbb502f13423fb85c341a8a --- /dev/null +++ b/cat-core/src/main/java/com/dianping/cat/abtest/mockit/GroupStrategyTester.java @@ -0,0 +1,128 @@ +package com.dianping.cat.abtest.mockit; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import junit.framework.Assert; + +import org.unidal.lookup.ComponentTestCase; +import org.unidal.test.mock.HttpServletRequestMock; +import org.unidal.test.mock.HttpServletResponseMock; +import org.xml.sax.SAXException; + +import com.dianping.cat.abtest.model.entity.AbtestModel; +import com.dianping.cat.abtest.model.entity.Case; +import com.dianping.cat.abtest.model.entity.Run; +import com.dianping.cat.abtest.model.transform.BaseVisitor; +import com.dianping.cat.abtest.model.transform.DefaultSaxParser; +import com.dianping.cat.abtest.spi.ABTestEntity; +import com.dianping.cat.abtest.spi.ABTestGroupStrategy; +import com.dianping.cat.abtest.spi.internal.DefaultABTestContext; + +public class GroupStrategyTester extends ComponentTestCase { + + protected int m_id = 1; + + protected String m_url = "abtest.xml"; + + private ABTestEntity m_entity; + + private ABTestGroupStrategy m_groupStrategy; + + private Map m_cookielets = new LinkedHashMap(); + + private boolean isFirst = true; + + public void assertGroupStrategy(HttpServletRequest req, HttpServletResponse res, + String expectedGroup) throws IOException, SAXException,Exception{ + + DefaultABTestContext context = initContext(); + + if (m_entity.isEligible(new Date())) { + if(isFirst){ + m_groupStrategy.init(); + isFirst = false; + } + m_groupStrategy.apply(context); + } + + Assert.assertEquals(expectedGroup, context.getGroupName()); + } + + private DefaultABTestContext initContext() throws IOException, SAXException { + ABTestEntity entity = buildEntity(); + + DefaultABTestContext context = new DefaultABTestContext(entity); + context.setCookielets(m_cookielets); + + if (!entity.isDisabled()) { + ABTestGroupStrategy groupStrategy = entity.getGroupStrategy(); + + context.setGroupStrategy(groupStrategy); + m_groupStrategy = groupStrategy; + } + + m_entity = context.getEntity(); + + return context; + } + + private ABTestEntity buildEntity() throws IOException, SAXException { + InputStream in = getClass().getResourceAsStream(m_url); + AbtestModel abtest = DefaultSaxParser.parse(in); + ABTestVisitor visitor = new ABTestVisitor(); + + abtest.accept(visitor); + return visitor.getEntity(); + } + + protected void setId(int id) { + m_id = id; + } + + protected void setUrl(String url) { + m_url = url; + } + + protected HttpServletRequestMock mockHttpRequest(){ + return null; + } + + protected HttpServletResponseMock mockHttpResponse(){ + return null; + } + + private class ABTestVisitor extends BaseVisitor { + private ABTestEntity m_entity; + + public ABTestEntity getEntity() { + return m_entity; + } + + @Override + public void visitCase(Case _case) { + for (Run run : _case.getRuns()) { + if (run.getId() == m_id) { + m_entity = new ABTestEntity(_case, run); + try { + ABTestGroupStrategy strategy = lookup( + ABTestGroupStrategy.class, + m_entity.getGroupStrategyName()); + strategy.init(); + m_entity.setGroupStrategy(strategy); + + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + } + } + +} diff --git a/cat-core/src/main/resources/META-INF/wizard/model/wizard.xml b/cat-core/src/main/resources/META-INF/wizard/model/wizard.xml index 552de89578e9813ae3d67d35bb0d414a3817a1ce..4e49860dfedc0056559d9b4ce9239f3b544efb67 100644 --- a/cat-core/src/main/resources/META-INF/wizard/model/wizard.xml +++ b/cat-core/src/main/resources/META-INF/wizard/model/wizard.xml @@ -6,4 +6,7 @@ src/test/resources/com/dianping/cat/message/configuration/server.xml + + src/test/resources/com/dianping/cat/abtest/spi/internal/abtest.xml + diff --git a/cat-home/pom.xml b/cat-home/pom.xml index 4c13e4af4d1c48ac40e8babf77cce14ca05ff1ce..197aabc990b08bb6100ec7952fb96e02812462de 100755 --- a/cat-home/pom.xml +++ b/cat-home/pom.xml @@ -1,217 +1,230 @@ - - - com.dianping.cat - parent - 0.6.2 - - 4.0.0 - cat-home - cat-home - war - - - org.unidal.framework - foundation-service - 2.0.3 - - - com.dianping.cat - cat-consumer - - - com.dianping.cat - cat-consumer-advanced - - - com.dianping.cat - cat-hadoop - - - org.springframework - spring - 2.5.6 - - - com.google.code.gson - gson - 1.6 - - - org.unidal.webres - WebResServer - 1.2.0 - - - org.unidal.framework - web-framework - 2.0.3 - - - javax.servlet - servlet-api - provided - - - javax.servlet - jstl - 1.2 - - - org.mortbay.jetty - jsp-api-2.1 - 6.1.14 - provided - - - org.mortbay.jetty - jsp-2.1 - provided - - - mysql - mysql-connector-java - runtime - - - org.freemarker - freemarker - 2.3.9 - - - junit - junit - test - - - org.mortbay.jetty - jetty - test - - - org.unidal.framework - test-framework - 2.0.3-SNAPSHOT - test - - - org.unidal.framework - dal-jdbc - 2.0.3 - - - org.apache.commons - commons-email - 1.1 - - - javax.mail - mail - 1.4.4 - - - - cat - - - ${basedir}/src/main/resources - true - - - - - org.apache.maven.plugins - maven-war-plugin - 2.1.1 - - - - src/main/resources - false - WEB-INF/classes - - - src/main/webapp - false - - WEB-INF/web.xml - - - - - - - org.unidal.maven.plugins - codegen-maven-plugin - 2.0.5 - - - generate plexus component descriptor - process-classes - - plexus - - - com.dianping.cat.build.ComponentsConfigurator - dev - - - - generate dal model files - generate-sources - - dal-model - - - - ${basedir}/src/main/resources/META-INF/dal/model/topology-graph-manifest.xml, - ${basedir}/src/main/resources/META-INF/dal/model/topology-graph-config-manifest.xml, - ${basedir}/src/main/resources/META-INF/dal/model/info-manifest.xml, - ${basedir}/src/main/resources/META-INF/dal/model/exception-threshold-config-manifest.xml, - ${basedir}/src/main/resources/META-INF/dal/model/bug-manifest.xml, - ${basedir}/src/main/resources/META-INF/dal/model/bug-config-manifest.xml, - ${basedir}/src/main/resources/META-INF/dal/model/service-report-manifest.xml, - ${basedir}/src/main/resources/META-INF/dal/model/heavy-report-manifest.xml, - ${basedir}/src/main/resources/META-INF/dal/model/utilization-report-manifest.xml, - ${basedir}/src/main/resources/META-INF/dal/model/utilization-config-manifest.xml, - - - - generate dal jdbc model - generate-sources - - dal-jdbc - - - + + com.dianping.cat + parent + 0.6.2 + + 4.0.0 + cat-home + cat-home + war + + + org.unidal.eunit + EunitTestFwk + 1.2.1 + test + + + org.unidal.framework + foundation-service + 2.0.3 + + + com.dianping.cat + cat-consumer + + + com.dianping.cat + cat-consumer-advanced + + + com.dianping.cat + cat-hadoop + + + org.springframework + spring + 2.5.6 + + + com.google.code.gson + gson + 1.6 + + + org.unidal.webres + WebResServer + 1.2.0 + + + org.unidal.framework + web-framework + 2.0.3 + + + javax.servlet + servlet-api + provided + + + javax.servlet + jstl + 1.2 + + + org.mortbay.jetty + jsp-api-2.1 + 6.1.14 + provided + + + org.mortbay.jetty + jsp-2.1 + provided + + + mysql + mysql-connector-java + runtime + + + org.freemarker + freemarker + 2.3.9 + + + junit + junit + test + + + org.mortbay.jetty + jetty + test + + + org.unidal.framework + test-framework + 2.0.3-SNAPSHOT + test + + + org.unidal.framework + dal-jdbc + 2.0.3 + + + org.apache.commons + commons-email + 1.1 + + + javax.mail + mail + 1.4.4 + + + com.google.code.javaparser + javaparser + 1.0.8 + + + + cat + + + ${basedir}/src/main/resources + true + + + + + org.apache.maven.plugins + maven-war-plugin + 2.1.1 + + + + src/main/resources + false + WEB-INF/classes + + + src/main/webapp + false + + WEB-INF/web.xml + + + + + + + org.unidal.maven.plugins + codegen-maven-plugin + 2.0.5 + + + generate plexus component descriptor + process-classes + + plexus + + + com.dianping.cat.build.ComponentsConfigurator + dev + + + + generate dal model files + generate-sources + + dal-model + + + + ${basedir}/src/main/resources/META-INF/dal/model/topology-graph-manifest.xml, + ${basedir}/src/main/resources/META-INF/dal/model/topology-graph-config-manifest.xml, + ${basedir}/src/main/resources/META-INF/dal/model/info-manifest.xml, + ${basedir}/src/main/resources/META-INF/dal/model/exception-threshold-config-manifest.xml, + ${basedir}/src/main/resources/META-INF/dal/model/bug-manifest.xml, + ${basedir}/src/main/resources/META-INF/dal/model/bug-config-manifest.xml, + ${basedir}/src/main/resources/META-INF/dal/model/service-report-manifest.xml, + ${basedir}/src/main/resources/META-INF/dal/model/heavy-report-manifest.xml, + ${basedir}/src/main/resources/META-INF/dal/model/utilization-report-manifest.xml, + ${basedir}/src/main/resources/META-INF/dal/model/utilization-config-manifest.xml, + + + + generate dal jdbc model + generate-sources + + dal-jdbc + + + - - - - - - org.apache.maven.plugins - maven-jar-plugin - 2.4 - - - true - - true - lib/ - com.dianping.cat.Server - - - - - - - - war - alpha - utf-8 - + ${basedir}/src/main/resources/META-INF/dal/jdbc/user-manifest.xml,]]> + + + + + + org.apache.maven.plugins + maven-jar-plugin + 2.4 + + + true + + true + lib/ + com.dianping.cat.Server + + + + + + + + war + alpha + utf-8 + diff --git a/cat-home/src/main/java/com/dianping/cat/build/ABTestComponentConfigurator.java b/cat-home/src/main/java/com/dianping/cat/build/ABTestComponentConfigurator.java index 49959afef82f8007b11e6e7a4c2613fbfc7c73b6..74b60ff18a564784d77beff18b7dd1d41a4aa884 100644 --- a/cat-home/src/main/java/com/dianping/cat/build/ABTestComponentConfigurator.java +++ b/cat-home/src/main/java/com/dianping/cat/build/ABTestComponentConfigurator.java @@ -6,8 +6,18 @@ import java.util.List; import org.unidal.lookup.configuration.AbstractResourceConfigurator; import org.unidal.lookup.configuration.Component; +import com.dianping.cat.consumer.advanced.MetricConfigManager; +import com.dianping.cat.core.dal.ProjectDao; import com.dianping.cat.home.dal.abtest.AbtestDao; +import com.dianping.cat.home.dal.abtest.AbtestReportDao; import com.dianping.cat.home.dal.abtest.AbtestRunDao; +import com.dianping.cat.home.dal.abtest.GroupStrategyDao; +import com.dianping.cat.system.page.abtest.GroupStrategyParser; +import com.dianping.cat.system.page.abtest.GsonBuilderManager; +import com.dianping.cat.system.page.abtest.ListViewHandler; +import com.dianping.cat.system.page.abtest.ReportHandler; +import com.dianping.cat.system.page.abtest.advisor.ABTestAdvisor; +import com.dianping.cat.system.page.abtest.advisor.DefaultABTestAdvisor; import com.dianping.cat.system.page.abtest.service.ABTestService; import com.dianping.cat.system.page.abtest.service.ABTestServiceImpl; @@ -18,10 +28,21 @@ public class ABTestComponentConfigurator extends AbstractResourceConfigurator { List all = new ArrayList(); - all.add(C(ABTestService.class, ABTestServiceImpl.class) - .req(AbtestDao.class).req(AbtestRunDao.class)); + all.add(C(GroupStrategyParser.class)); + + all.add(C(GsonBuilderManager.class)); + + all.add(C(ABTestAdvisor.class, DefaultABTestAdvisor.class)); + + all.add(C(ListViewHandler.class).req(AbtestDao.class).req(AbtestRunDao.class).config(E("pageSize").value("10"))); + + all.add(C(ReportHandler.class).req(AbtestDao.class).req(AbtestRunDao.class).req(AbtestReportDao.class) + .req(MetricConfigManager.class)); + + all.add(C(ABTestService.class, ABTestServiceImpl.class).req(AbtestDao.class).req(AbtestRunDao.class) + .req(GroupStrategyDao.class).req(ProjectDao.class).req(GsonBuilderManager.class) + .config(E("refreshTimeInSeconds").value("60"))); return all; } - } diff --git a/cat-home/src/main/java/com/dianping/cat/build/CatDatabaseConfigurator.java b/cat-home/src/main/java/com/dianping/cat/build/CatDatabaseConfigurator.java index 73ba5ffc971718f9e6660c227bb7b549b4b5cce4..a17a3354cfc4a53544631a72efe2511977de20a2 100644 --- a/cat-home/src/main/java/com/dianping/cat/build/CatDatabaseConfigurator.java +++ b/cat-home/src/main/java/com/dianping/cat/build/CatDatabaseConfigurator.java @@ -11,8 +11,7 @@ final class CatDatabaseConfigurator extends AbstractJdbcResourceConfigurator { public List defineComponents() { List all = new ArrayList(); - all.add(defineJdbcDataSourceComponent("cat", "com.mysql.jdbc.Driver", "jdbc:mysql://127.0.0.1:3306/cat", "root", "password", "")); - + all.add(defineJdbcDataSourceComponent("cat", "com.mysql.jdbc.Driver", "jdbc:mysql://127.0.0.1:3306/cat", "root", "", "")); defineSimpleTableProviderComponents(all, "cat", com.dianping.cat.home.dal.report._INDEX.getEntityClasses()); defineDaoComponents(all, com.dianping.cat.home.dal.report._INDEX.getDaoClasses()); diff --git a/cat-home/src/main/java/com/dianping/cat/build/TaskComponentConfigurator.java b/cat-home/src/main/java/com/dianping/cat/build/TaskComponentConfigurator.java index 3347706ed08b32aeab953627ea5a20bebead30be..8cf34b5eda9b081c5b5f07746f57acc23eb3ea8e 100644 --- a/cat-home/src/main/java/com/dianping/cat/build/TaskComponentConfigurator.java +++ b/cat-home/src/main/java/com/dianping/cat/build/TaskComponentConfigurator.java @@ -13,6 +13,7 @@ import com.dianping.cat.consumer.advanced.ProductLineConfigManager; import com.dianping.cat.core.dal.DailyGraphDao; import com.dianping.cat.core.dal.GraphDao; import com.dianping.cat.core.dal.TaskDao; +import com.dianping.cat.home.dal.abtest.AbtestReportDao; import com.dianping.cat.home.dal.report.BaselineDao; import com.dianping.cat.home.dal.report.TopologyGraphDao; import com.dianping.cat.report.baseline.BaselineConfigManager; @@ -25,6 +26,7 @@ import com.dianping.cat.report.page.model.spi.ModelService; import com.dianping.cat.report.page.transaction.TransactionMergeManager; import com.dianping.cat.report.service.ReportService; import com.dianping.cat.report.task.DefaultTaskConsumer; +import com.dianping.cat.report.task.abtest.ABTestReportBuilder; import com.dianping.cat.report.task.bug.BugReportBuilder; import com.dianping.cat.report.task.cross.CrossReportBuilder; import com.dianping.cat.report.task.dependency.DependencyReportBuilder; @@ -49,6 +51,7 @@ import com.dianping.cat.report.task.state.StateReportBuilder; import com.dianping.cat.report.task.transaction.TransactionGraphCreator; import com.dianping.cat.report.task.transaction.TransactionMerger; import com.dianping.cat.report.task.transaction.TransactionReportBuilder; +import com.dianping.cat.system.page.abtest.service.ABTestService; import com.dianping.cat.report.task.utilization.UtilizationReportBuilder; public class TaskComponentConfigurator extends AbstractResourceConfigurator { @@ -120,12 +123,15 @@ public class TaskComponentConfigurator extends AbstractResourceConfigurator { all.add(C(DependencyReportBuilder.class).req(ReportService.class, TopologyGraphBuilder.class, TopologyGraphDao.class)); + all.add(C(ABTestReportBuilder.class).req(ReportService.class, AbtestReportDao.class, + ProductLineConfigManager.class, ABTestService.class)); + all.add(C(ReportFacade.class)// .req(TransactionReportBuilder.class, EventReportBuilder.class, ProblemReportBuilder.class, HeartbeatReportBuilder.class, MatrixReportBuilder.class, CrossReportBuilder.class, SqlReportBuilder.class, StateReportBuilder.class, DependencyReportBuilder.class, BugReportBuilder.class, ServiceReportBuilder.class, MetricBaselineReportBuilder.class, - HeavyReportBuilder.class, UtilizationReportBuilder.class)); + HeavyReportBuilder.class, UtilizationReportBuilder.class, ABTestReportBuilder.class)); return all; } diff --git a/cat-home/src/main/java/com/dianping/cat/report/page/metric/MetricDisplay.java b/cat-home/src/main/java/com/dianping/cat/report/page/metric/MetricDisplay.java index 88a8ce0cc5d9e0d9adf65d5728f844d0bf7610e3..bae45a9966a12de1a7a6d6b9e2e2c8afe38180de 100644 --- a/cat-home/src/main/java/com/dianping/cat/report/page/metric/MetricDisplay.java +++ b/cat-home/src/main/java/com/dianping/cat/report/page/metric/MetricDisplay.java @@ -236,4 +236,4 @@ public class MetricDisplay extends BaseVisitor { return result; } -} \ No newline at end of file +} diff --git a/cat-home/src/main/java/com/dianping/cat/report/page/metric/MetricDisplayMerger.java b/cat-home/src/main/java/com/dianping/cat/report/page/metric/MetricDisplayMerger.java index d266b9eb8efabc01033b6badd0b8c31a09ba5c0e..d641a529690a2405fdeb3f2dcbf2ad250430f953 100644 --- a/cat-home/src/main/java/com/dianping/cat/report/page/metric/MetricDisplayMerger.java +++ b/cat-home/src/main/java/com/dianping/cat/report/page/metric/MetricDisplayMerger.java @@ -81,7 +81,7 @@ public class MetricDisplayMerger extends BaseVisitor { private com.dianping.cat.home.dal.abtest.Abtest findAbTest(int id) { com.dianping.cat.home.dal.abtest.Abtest abtest = null; if (id >= 0) { - abtest = m_abtestService.getABTestNameByRunId(id); + abtest = m_abtestService.getABTestByRunId(id); } if (abtest == null) { abtest = new com.dianping.cat.home.dal.abtest.Abtest(); diff --git a/cat-home/src/main/java/com/dianping/cat/report/task/abtest/ABTestReportBuilder.java b/cat-home/src/main/java/com/dianping/cat/report/task/abtest/ABTestReportBuilder.java new file mode 100644 index 0000000000000000000000000000000000000000..f2dbd8506d52b65f3ab10fb50e5ec6c40bede7d4 --- /dev/null +++ b/cat-home/src/main/java/com/dianping/cat/report/task/abtest/ABTestReportBuilder.java @@ -0,0 +1,168 @@ +package com.dianping.cat.report.task.abtest; + +import java.util.Calendar; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.codehaus.plexus.personality.plexus.lifecycle.phase.Initializable; +import org.codehaus.plexus.personality.plexus.lifecycle.phase.InitializationException; +import org.unidal.dal.jdbc.DalException; +import org.unidal.lookup.annotation.Inject; + +import com.dianping.cat.Cat; +import com.dianping.cat.consumer.advanced.ProductLineConfigManager; +import com.dianping.cat.consumer.metric.model.entity.MetricReport; +import com.dianping.cat.helper.TimeUtil; +import com.dianping.cat.home.dal.abtest.AbtestReportDao; +import com.dianping.cat.home.dal.abtest.AbtestReportEntity; +import com.dianping.cat.home.dal.abtest.AbtestRun; +import com.dianping.cat.report.abtest.entity.AbtestReport; +import com.dianping.cat.report.service.ReportService; +import com.dianping.cat.report.task.spi.ReportTaskBuilder; +import com.dianping.cat.system.page.abtest.AbtestStatus; +import com.dianping.cat.system.page.abtest.service.ABTestService; + +public class ABTestReportBuilder implements ReportTaskBuilder, Initializable { + @Inject + protected ReportService m_reportService; + + @Inject + private AbtestReportDao m_abtestReportDao; + + @Inject + private ProductLineConfigManager m_productLineConfigManager; + + @Inject + private ABTestService m_abtestService; + + private Calendar m_calendar = Calendar.getInstance(); + + @Override + public boolean buildHourlyTask(String name, String domain, Date period) { + List runs = m_abtestService.getAbtestRunByStatus(AbtestStatus.RUNNING); + + for (AbtestRun run : runs) { + Set productLineSet = getProductLinesByRunID(run); + + buildHourlyTaskInternal(period, productLineSet); + } + + return true; + } + + private Date resetTime(String period, Date time) { + m_calendar.setTime(time); + m_calendar.set(Calendar.MINUTE, 0); + m_calendar.set(Calendar.SECOND, 0); + m_calendar.set(Calendar.MILLISECOND, 0); + + if (period.equals("day")) { + m_calendar.set(Calendar.HOUR_OF_DAY, 0); + } + + return m_calendar.getTime(); + } + + @Override + public boolean buildDailyTask(String name, String domain, Date period) { + throw new UnsupportedOperationException("ABTest report don't support daily report!"); + } + + @Override + public boolean buildMonthlyTask(String name, String domain, Date period) { + throw new UnsupportedOperationException("ABTest line report don't support monthly report!"); + } + + @Override + public boolean buildWeeklyTask(String name, String domain, Date period) { + throw new UnsupportedOperationException("ABTest line report don't support weekly report!"); + } + + @Override + public void initialize() throws InitializationException { + Date now = resetTime("hour", new Date()); + List runs = m_abtestService.getAbtestRunByStatus(AbtestStatus.RUNNING); + + for (AbtestRun run : runs) { + Set productLineSet = getProductLinesByRunID(run); + Date period = getLatestPeriod(now, run.getId()); + + m_calendar.setTime(period); + + while (!period.after(now)) { + buildHourlyTaskInternal(period, productLineSet); + + m_calendar.add(Calendar.HOUR, 1); + period = m_calendar.getTime(); + } + } + } + + private Date getLatestPeriod(Date now, int runId) { + com.dianping.cat.home.dal.abtest.AbtestReport latestReport = null; + Date period = null; + + try { + latestReport = m_abtestReportDao.findLatestReportByRunId(runId, AbtestReportEntity.READSET_FULL); + } catch (Exception e) { + // ignore it + } + + if (latestReport == null) { + m_calendar.setTime(now); + m_calendar.add(Calendar.DAY_OF_MONTH, -14); + + period = m_calendar.getTime(); + } else { + period = latestReport.getPeriod(); + } + + return period; + } + + private Set getProductLinesByRunID(AbtestRun run) { + String[] domains = run.getDomains().split(","); + Set productLineSet = new HashSet(); + + for (String _domain : domains) { + String productLine = m_productLineConfigManager.queryProductLineByDomain(_domain); + + if (!productLine.equals("Default")) { + productLineSet.add(productLine); + } + } + return productLineSet; + } + + private void buildHourlyTaskInternal(Date period, Set productLineSet) { + MetricReportForABTestVisitor visitor = new MetricReportForABTestVisitor(); + + for (String productLine : productLineSet) { + MetricReport metricReport = m_reportService.queryMetricReport(productLine, period, new Date(period.getTime() + + TimeUtil.ONE_HOUR)); + + metricReport.accept(visitor); + } + + Map result = visitor.getReportMap(); + + for (AbtestReport report : result.values()) { + if (report.getRunId() != -1) { + com.dianping.cat.home.dal.abtest.AbtestReport _report = new com.dianping.cat.home.dal.abtest.AbtestReport(); + + _report.setRunId(report.getRunId()); + _report.setPeriod(period); + _report.setContent(report.toString()); + + try { + m_abtestReportDao.insert(_report); + } catch (DalException e) { + Cat.logError(e); + } + } + } + } +} diff --git a/cat-home/src/main/java/com/dianping/cat/report/task/abtest/MetricReportForABTestVisitor.java b/cat-home/src/main/java/com/dianping/cat/report/task/abtest/MetricReportForABTestVisitor.java new file mode 100644 index 0000000000000000000000000000000000000000..38066f48a242fce23c7c5c85dc37517c6927bd4e --- /dev/null +++ b/cat-home/src/main/java/com/dianping/cat/report/task/abtest/MetricReportForABTestVisitor.java @@ -0,0 +1,139 @@ +package com.dianping.cat.report.task.abtest; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import com.dianping.cat.consumer.metric.model.entity.Abtest; +import com.dianping.cat.consumer.metric.model.entity.Group; +import com.dianping.cat.consumer.metric.model.entity.MetricItem; +import com.dianping.cat.consumer.metric.model.entity.MetricReport; +import com.dianping.cat.consumer.metric.model.entity.Point; +import com.dianping.cat.consumer.metric.model.transform.BaseVisitor; +import com.dianping.cat.report.abtest.entity.AbtestReport; +import com.dianping.cat.report.abtest.entity.Goal; +import com.dianping.cat.report.abtest.entity.Variation; + +public class MetricReportForABTestVisitor extends BaseVisitor { + + private Map m_reportMap; + + private Map> m_metrics; + + private String m_id; + + private String m_type; + + private int m_runId; + + private String m_variation; + + private Date m_startDate; + + private Date m_endDate; + + public MetricReportForABTestVisitor() { + m_reportMap = new HashMap(); + m_metrics = new HashMap>(); + } + + private AbtestReport findOrCreateAbtestReport(int runId) { + AbtestReport report = m_reportMap.get(runId); + + if (report == null) { + report = new AbtestReport(); + + report.setRunId(runId); + report.setStartTime(m_startDate); + report.setEndTime(m_endDate); + m_reportMap.put(runId, report); + } + + return report; + } + + public Map getReportMap() { + for (AbtestReport report : m_reportMap.values()) { + HashMap map = m_metrics.get(report.getRunId()); + + for (String metric : map.keySet()) { + for (Variation variation : report.getVariations().values()) { + Goal goal = variation.findOrCreateGoal(metric); + + goal.setType(map.get(metric)); + } + + Goal goal = new Goal(); + + goal.setName(metric); + report.getGoals().add(goal); + } + } + + return m_reportMap; + } + + @Override + public void visitAbtest(Abtest abtest) { + try { + m_runId = Integer.parseInt(abtest.getRunId()); + } catch (Exception e) { + m_runId = -1; + } + + HashMap map = m_metrics.get(m_runId); + + if (map == null) { + map = new HashMap(); + + m_metrics.put(m_runId, map); + } + + map.put(m_id, m_type); + super.visitAbtest(abtest); + } + + @Override + public void visitGroup(Group group) { + m_variation = group.getName(); + + if(m_variation.length() == 0){ + m_variation = "Control"; + } + + super.visitGroup(group); + } + + @Override + public void visitMetricItem(MetricItem metricItem) { + m_id = metricItem.getId(); + m_type = metricItem.getType(); + + super.visitMetricItem(metricItem); + } + + @Override + public void visitMetricReport(MetricReport metricReport) { + m_startDate = metricReport.getStartTime(); + m_endDate = metricReport.getEndTime(); + + super.visitMetricReport(metricReport); + } + + @Override + public void visitPoint(Point point) { + AbtestReport report = findOrCreateAbtestReport(m_runId); + + Variation variation = report.findOrCreateVariation(m_variation); + + Goal goal = variation.findOrCreateGoal(m_id); + + int count = goal.getCount() + point.getCount(); + double sum = goal.getSum() + point.getSum(); + + goal.setType(m_type); + goal.setCount(count); + goal.setSum(sum); + // avg? + } +} diff --git a/cat-home/src/main/java/com/dianping/cat/report/task/spi/ReportFacade.java b/cat-home/src/main/java/com/dianping/cat/report/task/spi/ReportFacade.java index 99fa659b23f4d4c2d28e4c297413114cf7b9ab78..37411d8761598af204c98f35214f64c728f00641 100644 --- a/cat-home/src/main/java/com/dianping/cat/report/task/spi/ReportFacade.java +++ b/cat-home/src/main/java/com/dianping/cat/report/task/spi/ReportFacade.java @@ -24,6 +24,7 @@ import com.dianping.cat.consumer.sql.SqlAnalyzer; import com.dianping.cat.consumer.state.StateAnalyzer; import com.dianping.cat.consumer.transaction.TransactionAnalyzer; import com.dianping.cat.core.dal.Task; +import com.dianping.cat.report.task.abtest.ABTestReportBuilder; import com.dianping.cat.report.task.bug.BugReportBuilder; import com.dianping.cat.report.task.cross.CrossReportBuilder; import com.dianping.cat.report.task.dependency.DependencyReportBuilder; @@ -83,7 +84,10 @@ public class ReportFacade implements LogEnabled, Initializable { private DependencyReportBuilder m_dependendcyReportBuilder; @Inject - private MetricBaselineReportBuilder m_metricBaselineReportBuilder; + private MetricBaselineReportBuilder m_metricBaselineReportBuilder; + + @Inject + private ABTestReportBuilder m_abtestReportBuilder; @Inject private HeavyReportBuilder m_heavyReportBuilder; @@ -165,6 +169,7 @@ public class ReportFacade implements LogEnabled, Initializable { m_reportBuilders.put(Constants.REPORT_SERVICE, m_serviceReportBuilder); m_reportBuilders.put(Constants.REPORT_HEAVY, m_heavyReportBuilder); m_reportBuilders.put(Constants.REPORT_UTILIZATION, m_utilizationReportBuilder); + m_reportBuilders.put(Constants.ABTEST, m_abtestReportBuilder); } } diff --git a/cat-home/src/main/java/com/dianping/cat/system/page/abtest/ABTestReport.java b/cat-home/src/main/java/com/dianping/cat/system/page/abtest/ABTestReport.java deleted file mode 100644 index ae7590b9cefd298905d14f6cf4766095c8e2e4a7..0000000000000000000000000000000000000000 --- a/cat-home/src/main/java/com/dianping/cat/system/page/abtest/ABTestReport.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.dianping.cat.system.page.abtest; - -import java.util.Date; -import java.util.Map; - -import com.dianping.cat.home.dal.abtest.Abtest; -import com.dianping.cat.home.dal.abtest.AbtestRun; - -public class ABTestReport { - - private Abtest m_entity; - - private AbtestRun m_run; - - private AbtestStatus m_status; - - private Map m_items; - - public ABTestReport(Abtest entity, AbtestRun run) { - m_entity = entity; - m_run = run; - } - - public ABTestReport(Abtest entity, AbtestRun run, Date now) { - m_entity = entity; - m_run = run; - setStatus(now); - // TODO m_items setting - } - - private void setStatus(Date now) { - m_status = AbtestStatus.calculateStatus(m_run, now); - } - - public Abtest getEntity() { - return m_entity; - } - - public AbtestRun getRun() { - return m_run; - } - - public void setStatus(AbtestStatus status) { - m_status = status; - } - - public AbtestStatus getStatus() { - return m_status; - } - - public Map getItems() { - return m_items; - } - -} diff --git a/cat-home/src/main/java/com/dianping/cat/system/page/abtest/AbtestStatus.java b/cat-home/src/main/java/com/dianping/cat/system/page/abtest/AbtestStatus.java index 10e4730d0ada146f3cf771d41cf83e8abe8c7830..f6b456c06e3c25ae60473a8d1fc7224c11e7f885 100644 --- a/cat-home/src/main/java/com/dianping/cat/system/page/abtest/AbtestStatus.java +++ b/cat-home/src/main/java/com/dianping/cat/system/page/abtest/AbtestStatus.java @@ -10,9 +10,9 @@ public enum AbtestStatus { CREATED, READY, RUNNING, TERMINATED, SUSPENDED; private static final int s_deltaTime = -1; // -1 hour - + private static final Calendar calendar = Calendar.getInstance(); - + public static AbtestStatus getByName(String name, AbtestStatus defaultStatus) { for (AbtestStatus status : AbtestStatus.values()) { if (status.name().equalsIgnoreCase(name)) { @@ -67,9 +67,8 @@ public enum AbtestStatus { } } - public String getStatus() { - return name().toLowerCase(); - } + return name().toLowerCase(); + } } diff --git a/cat-home/src/main/java/com/dianping/cat/system/page/abtest/Action.java b/cat-home/src/main/java/com/dianping/cat/system/page/abtest/Action.java index 41a6e518aab6bae86e5a4f33818a82dd165e447f..3dfb08acde01f1cecca1a8c7cd123f88c47cb657 100644 --- a/cat-home/src/main/java/com/dianping/cat/system/page/abtest/Action.java +++ b/cat-home/src/main/java/com/dianping/cat/system/page/abtest/Action.java @@ -9,7 +9,17 @@ public enum Action implements org.unidal.web.mvc.Action { MODEL("model"), - REPORT("report"); + REPORT("report"), + + AJAX_CREATE("ajax_create"), + + AJAX_DETAIL("ajax_detail"), + + AJAX_ADDGROUPSTRATEGY("ajax_addGs"), + + AJAX_PARSEGROUPSTRATEGY("ajax_parseGs"), + + ABTEST_CACULATOR("caculator"); private String m_name; diff --git a/cat-home/src/main/java/com/dianping/cat/system/page/abtest/Context.java b/cat-home/src/main/java/com/dianping/cat/system/page/abtest/Context.java index af98ec6b70c8efc23ee0204151143745460565e7..5f48c1d8565dc06dbf8713e9a90a98d878ccd091 100644 --- a/cat-home/src/main/java/com/dianping/cat/system/page/abtest/Context.java +++ b/cat-home/src/main/java/com/dianping/cat/system/page/abtest/Context.java @@ -1,7 +1,29 @@ package com.dianping.cat.system.page.abtest; +import java.util.List; + import com.dianping.cat.system.SystemContext; +import com.dianping.cat.system.page.abtest.advisor.ABTestAdvice; public class Context extends SystemContext { + + private String m_responseJson; + + private List m_advices; + + public String getResponseJson() { + return m_responseJson; + } + + public void setResponseJson(String responseJson) { + m_responseJson = responseJson; + } + public void setAdvice(List advices) { + m_advices = advices; + } + + public List getAdvice(){ + return m_advices; + } } diff --git a/cat-home/src/main/java/com/dianping/cat/system/page/abtest/GroupStrategyParser.java b/cat-home/src/main/java/com/dianping/cat/system/page/abtest/GroupStrategyParser.java new file mode 100644 index 0000000000000000000000000000000000000000..00a050fc008508ccc39b4adbac4b9a29fef731dc --- /dev/null +++ b/cat-home/src/main/java/com/dianping/cat/system/page/abtest/GroupStrategyParser.java @@ -0,0 +1,86 @@ +package com.dianping.cat.system.page.abtest; + +import japa.parser.JavaParser; +import japa.parser.ParseException; +import japa.parser.ast.CompilationUnit; +import japa.parser.ast.ImportDeclaration; +import japa.parser.ast.body.FieldDeclaration; +import japa.parser.ast.body.TypeDeclaration; +import japa.parser.ast.body.VariableDeclarator; +import japa.parser.ast.expr.AnnotationExpr; +import japa.parser.ast.visitor.VoidVisitorAdapter; + +import java.io.InputStream; +import java.util.List; + +import com.dianping.cat.abtest.model.entity.Field; +import com.dianping.cat.abtest.model.entity.GroupstrategyDescriptor; + +public class GroupStrategyParser { + public GroupstrategyDescriptor parse(InputStream input) throws ParseException { + final CompilationUnit result = JavaParser.parse(input); + + TypeDeclaration type = (TypeDeclaration) result.getTypes().get(0); + + final GroupstrategyDescriptor descriptor = new GroupstrategyDescriptor(); + String name = type.getName(); + + descriptor.setClassName(name); + descriptor.setFullyQualifiedName(result.getPackage().getName() + "." + name); + + new AnnotationVisitor(descriptor).visit(result, null); + return descriptor; + } + + class AnnotationVisitor extends VoidVisitorAdapter { + private GroupstrategyDescriptor m_descriptor; + + private boolean m_isImportInjectClass = false; + + public AnnotationVisitor(GroupstrategyDescriptor descriptor) { + m_descriptor = descriptor; + } + + public void visit(FieldDeclaration node, Object arg) { + if (m_isImportInjectClass) { + List annotations = node.getAnnotations(); + + if (annotations != null) { + for (AnnotationExpr expr : annotations) { + String annotation = expr.toString(); + + int pos = annotation.indexOf("@Inject"); + + System.out.println(expr.getData()); + if (pos >= 0) { + int begin = annotation.indexOf('"'); + int end = annotation.lastIndexOf('"'); + + String name = annotation.substring(begin + 1, end).trim(); + String type = node.getType().toString(); + List modifierName = node.getVariables(); + VariableDeclarator firstVar = modifierName.get(0); + + Field field = new Field(); + field.setName(name); + field.setType(type); + field.setModifierName(firstVar.getId().getName()); + + if (firstVar.getInit() != null) { + field.setValue(firstVar.getInit().toString()); + } + + m_descriptor.getFields().add(field); + } + } + } + } + } + + public void visit(ImportDeclaration n, Object arg) { + if (n.getName().toString().equals("org.unidal.lookup.annotation.Inject")) { + m_isImportInjectClass = true; + } + } + } +} diff --git a/cat-home/src/main/java/com/dianping/cat/system/page/abtest/GsonBuilderManager.java b/cat-home/src/main/java/com/dianping/cat/system/page/abtest/GsonBuilderManager.java new file mode 100644 index 0000000000000000000000000000000000000000..7902012e5bd3b9871d05722b2a67779208e96109 --- /dev/null +++ b/cat-home/src/main/java/com/dianping/cat/system/page/abtest/GsonBuilderManager.java @@ -0,0 +1,26 @@ +package com.dianping.cat.system.page.abtest; + +import com.google.gson.FieldNamingStrategy; +import com.google.gson.GsonBuilder; + +public class GsonBuilderManager { + private static GsonBuilder s_gsonBuilder = new GsonBuilder(); + + static { + s_gsonBuilder.setFieldNamingStrategy(new NonPrexFieldNamingStrategy()); + } + + public GsonBuilder getGsonBuilder() { + return s_gsonBuilder; + } + + public static class NonPrexFieldNamingStrategy implements FieldNamingStrategy { + @Override + public String translateName(java.lang.reflect.Field f) { + String name = f.getName(); + int pos = name.indexOf('_'); + + return name.substring(pos + 1); + } + } +} diff --git a/cat-home/src/main/java/com/dianping/cat/system/page/abtest/Handler.java b/cat-home/src/main/java/com/dianping/cat/system/page/abtest/Handler.java index 225b6b7dbebef62f8bf63a3c57d96eef56ccf731..b914f80ea800f260192ec12695eee714fb684b37 100644 --- a/cat-home/src/main/java/com/dianping/cat/system/page/abtest/Handler.java +++ b/cat-home/src/main/java/com/dianping/cat/system/page/abtest/Handler.java @@ -1,18 +1,25 @@ package com.dianping.cat.system.page.abtest; +import java.io.ByteArrayInputStream; import java.io.IOException; +import java.io.InputStream; +import java.io.StringWriter; import java.util.ArrayList; import java.util.Date; +import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.TreeMap; import javax.servlet.ServletException; import org.apache.commons.lang.StringUtils; import org.codehaus.plexus.logging.LogEnabled; import org.codehaus.plexus.logging.Logger; +import org.codehaus.plexus.personality.plexus.lifecycle.phase.Initializable; +import org.codehaus.plexus.personality.plexus.lifecycle.phase.InitializationException; import org.unidal.dal.jdbc.DalException; +import org.unidal.helper.Threads; +import org.unidal.helper.Threads.Task; import org.unidal.lookup.annotation.Inject; import org.unidal.web.mvc.ErrorObject; import org.unidal.web.mvc.PageHandler; @@ -23,10 +30,11 @@ import org.unidal.web.mvc.annotation.PayloadMeta; import com.dianping.cat.Cat; import com.dianping.cat.abtest.model.entity.AbtestModel; import com.dianping.cat.abtest.model.entity.Case; +import com.dianping.cat.abtest.model.entity.Condition; +import com.dianping.cat.abtest.model.entity.GroupstrategyDescriptor; import com.dianping.cat.abtest.model.entity.Run; import com.dianping.cat.core.dal.Project; -import com.dianping.cat.core.dal.ProjectDao; -import com.dianping.cat.core.dal.ProjectEntity; +import com.dianping.cat.home.abtest.ScriptFragementTest; import com.dianping.cat.home.dal.abtest.Abtest; import com.dianping.cat.home.dal.abtest.AbtestDao; import com.dianping.cat.home.dal.abtest.AbtestEntity; @@ -37,9 +45,18 @@ import com.dianping.cat.home.dal.abtest.GroupStrategy; import com.dianping.cat.home.dal.abtest.GroupStrategyDao; import com.dianping.cat.home.dal.abtest.GroupStrategyEntity; import com.dianping.cat.system.SystemPage; -import com.dianping.cat.system.page.abtest.Model.AbtestDaoModel; +import com.dianping.cat.system.page.abtest.ListViewModel.AbtestItem; +import com.dianping.cat.system.page.abtest.advisor.ABTestAdvice; +import com.dianping.cat.system.page.abtest.advisor.ABTestAdvisor; +import com.dianping.cat.system.page.abtest.conditions.URLScriptProvider; +import com.dianping.cat.system.page.abtest.service.ABTestService; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; -public class Handler implements PageHandler, LogEnabled { +import freemarker.template.Configuration; +import freemarker.template.Template; + +public class Handler implements PageHandler, LogEnabled, Initializable { public static final String CHARSET = "UTF-8"; @@ -53,103 +70,142 @@ public class Handler implements PageHandler, LogEnabled { private GroupStrategyDao m_groupStrategyDao; @Inject - private ProjectDao m_projectDao; + private JspViewer m_jspViewer; @Inject - private JspViewer m_jspViewer; + private GroupStrategyParser m_parser; + + @Inject + private ABTestService m_service; + + @Inject + private ABTestAdvisor m_advisor; + + @Inject + private ListViewHandler m_listViewHandler; + + @Inject + private ReportHandler m_reportHandler; + + @Inject + private GsonBuilderManager m_gsonBuilderManager; private Logger m_logger; - private final int m_pageSize = 10; + private Configuration m_configuration; + + private URLScriptProvider m_urlScriptProvider; @Override public void enableLogging(Logger logger) { m_logger = logger; } - private AbtestModel fetchAbtestModel() { + private void handleCreateOrUpdateAction(Context ctx, Payload payload, boolean isUpdate) { try { - AbtestModel abtestModel = new AbtestModel(); - - List abtestRuns = m_abtestRunDao.findAll(AbtestRunEntity.READSET_FULL); - - if (abtestRuns != null) { - Date now = new Date(); - for (AbtestRun abtestRun : abtestRuns) { - AbtestStatus status = AbtestStatus.calculateStatus(abtestRun, now); - if (status == AbtestStatus.READY || status == AbtestStatus.RUNNING) { - // fetch Case and GroupStrategy - int caseId = abtestRun.getCaseId(); - Abtest entity = m_abtestDao.findByPK(caseId, AbtestEntity.READSET_FULL); - int gid = entity.getGroupStrategy(); - GroupStrategy groupStrategy = m_groupStrategyDao.findByPK(gid, GroupStrategyEntity.READSET_FULL); - - Case _case = transform(abtestRun, entity, groupStrategy); - abtestModel.addCase(_case); - } - } + int caseID = 0; + + if (!isUpdate) { + Abtest abtest = new Abtest(); + + abtest.setName(payload.getName()); + abtest.setOwner(payload.getOwner()); + abtest.setDescription(payload.getDescription()); + abtest.setGroupStrategy(payload.getStrategyId()); + abtest.setDomains(StringUtils.join(payload.getDomains(), ',')); + + m_abtestDao.insert(abtest); + m_service.setModified(); + caseID = abtest.getId(); } - return abtestModel; - } catch (DalException e) { - m_logger.error("Error when find all AbtestRun", e); - Cat.logError(e); - } - return null; - } + AbtestRun run = new AbtestRun(); + Date now = new Date(); - private List getAllGroupStrategys() { - try { - return m_groupStrategyDao.findAllByStatus(1, GroupStrategyEntity.READSET_FULL); + if (isUpdate) { + run.setId(payload.getId()); + run.setKeyId(payload.getId()); + } else { + run.setCaseId(caseID); + run.setCreationDate(now); + } + run.setCreator(payload.getOwner()); + run.setStartDate(payload.getStartDate()); + run.setEndDate(payload.getEndDate()); + run.setDomains(StringUtils.join(payload.getDomains(), ',')); + run.setStrategyConfiguration(payload.getStrategyConfig()); + run.setConditions(payload.getConditions()); + run.setConversionGoals(payload.getConversionGoals()); + run.setJavaFragement(getJavaFragement(run)); + run.setDisabled(false); + run.setModifiedDate(now); + + if (isUpdate) { + // only update run info, do not update abtest meta-info + m_abtestRunDao.updateByPK(run, AbtestRunEntity.UPDATESET_ALLOWED_MODIFYPART); + m_service.setModified(); + ctx.setResponseJson(responseJson(0, "successfully modify a abtest!")); + } else { + m_abtestRunDao.insert(run); + m_service.setModified(); + ctx.setResponseJson(responseJson(0, "successfully create a abtest!")); + } } catch (DalException e) { - m_logger.error(e.getMessage(), e); Cat.logError(e); + ctx.setResponseJson(responseJson(1, e.getMessage())); } - return null; } - private Map> getAllProjects() { - List projects = new ArrayList(); + private String getJavaFragement(AbtestRun run) { + Gson gson = m_gsonBuilderManager.getGsonBuilder().create(); + List conditions = gson.fromJson(run.getConditions(), new TypeToken>() { + }.getType()); + Run abstractRun = new Run(); + Map root = new HashMap(); + + abstractRun.getConditions().addAll(conditions); + + root.put("run", abstractRun); + root.put("urlScriptProvider", m_urlScriptProvider); + + StringWriter sw = new StringWriter(5000); try { - projects = m_projectDao.findAll(ProjectEntity.READSET_FULL); + Template t = m_configuration.getTemplate("scriptFragement.ftl"); + + t.process(root, sw); } catch (Exception e) { - m_logger.error(e.getMessage(), e); Cat.logError(e); + e.printStackTrace(); } - // Collections.sort(projects, new ProjectCompartor()); - return transform(projects); + return sw.toString(); } - private void handleCreateAction(Context ctx, Payload payload) { - Abtest abtest = new Abtest(); - - abtest.setName(payload.getName()); - abtest.setOwner(payload.getOwner()); - abtest.setDescription(payload.getDescription()); - abtest.setGroupStrategy(payload.getStrategyId()); - abtest.setDomains(StringUtils.join(payload.getDomains(), ',')); - - AbtestRun run = new AbtestRun(); - - run.setCreator(payload.getOwner()); - run.setStartDate(payload.getStartDate()); - run.setEndDate(payload.getEndDate()); - run.setDomains(StringUtils.join(payload.getDomains(), ',')); - run.setStrategyConfiguration(payload.getStrategyConfig()); - run.setDisabled(false); - Date now = new Date(); - run.setCreationDate(now); - run.setModifiedDate(now); + private void handleCreateGroupStrategyAction(Context ctx, Payload payload) { + GroupStrategy gs = new GroupStrategy(); + + String name = payload.getGroupStrategyName(); + gs.setClassName(payload.getGroupStrategyClassName()); + gs.setName(name); + gs.setFullyQualifiedName(payload.getGroupStrategyFullName()); + gs.setDescriptor(payload.getGroupStrategyDescriptor()); + gs.setDescription(payload.getGroupStrategyDescription()); + gs.setStatus(1); + try { - m_abtestDao.insert(abtest); + List groupStrategies = m_groupStrategyDao.findByName(name, GroupStrategyEntity.READSET_FULL); - run.setCaseId(abtest.getId()); - m_abtestRunDao.insert(run); + if (groupStrategies == null || groupStrategies.size() == 0) { + m_groupStrategyDao.insert(gs); + m_service.setModified(); + } else { + throw new DalException("Aready to has a groupstrategy which has the same name..."); + } + + ctx.setResponseJson(responseJson(0, "successfully create a groupstrategy!")); } catch (DalException e) { - m_logger.error("Error when saving abtest", e); Cat.logError(e); - ctx.setException(e); + ctx.setResponseJson(responseJson(1, e.getMessage())); } } @@ -158,23 +214,39 @@ public class Handler implements PageHandler, LogEnabled { @InboundActionMeta(name = "abtest") public void handleInbound(Context ctx) throws ServletException, IOException { if (ctx.getException() != null) { + ctx.setResponseJson(responseJson(1, ctx.getException().getMessage())); return; } Payload payload = ctx.getPayload(); Action action = payload.getAction(); + if (action == Action.VIEW) { + handleStatusChangeAction(ctx); + } + if (ctx.getHttpServletRequest().getMethod().equalsIgnoreCase("post")) { - if (action == Action.CREATE) { - handleCreateAction(ctx, payload); - } else if (action == Action.DETAIL) { - handleUpdateAction(ctx, payload); + if (action == Action.AJAX_CREATE) { + handleCreateOrUpdateAction(ctx, payload, false); + } else if (action == Action.AJAX_DETAIL) { + handleCreateOrUpdateAction(ctx, payload, true); + } else if (action == Action.AJAX_ADDGROUPSTRATEGY) { + handleCreateGroupStrategyAction(ctx, payload); + } else if (action == Action.AJAX_PARSEGROUPSTRATEGY) { + handleParseGroupStrategyAction(ctx, payload); + } else if (action == Action.ABTEST_CACULATOR) { + handleCaculatorAction(ctx, payload); } } + } - if (action == Action.VIEW) { - handleStatusChangeAction(ctx); - } + private void handleCaculatorAction(Context ctx, Payload payload) { + float actualCtr = payload.getConversionRate() / 100.00f; + + m_advisor.setCurrentPv(payload.getPv()); + List advices = m_advisor.offer(actualCtr, actualCtr + 0.10f); + + ctx.setAdvice(advices); } @Override @@ -186,7 +258,7 @@ public class Handler implements PageHandler, LogEnabled { switch (action) { case VIEW: - renderListModel(model, payload); + m_listViewHandler.handle(ctx, model, payload); break; case CREATE: renderCreateModel(model); @@ -195,10 +267,10 @@ public class Handler implements PageHandler, LogEnabled { renderDetailModel(ctx, model, payload); break; case REPORT: - renderReportModel(ctx, model, payload); + m_reportHandler.handle(ctx, model, payload); break; case MODEL: - renderModel(model); + renderModel(model, payload); break; } @@ -207,6 +279,19 @@ public class Handler implements PageHandler, LogEnabled { m_jspViewer.view(ctx, model); } + private void handleParseGroupStrategyAction(Context ctx, Payload payload) { + InputStream stream; + try { + stream = new ByteArrayInputStream(payload.getSrcCode().getBytes("UTF-8")); + GroupstrategyDescriptor descriptor = m_parser.parse(stream); + + Gson gson = m_gsonBuilderManager.getGsonBuilder().create(); + ctx.setResponseJson(gson.toJson(descriptor, GroupstrategyDescriptor.class)); + } catch (Throwable e) { + ctx.setResponseJson("{}"); + } + } + private void handleStatusChangeAction(Context ctx) { Payload payload = ctx.getPayload(); ErrorObject error = new ErrorObject("disable"); @@ -223,6 +308,7 @@ public class Handler implements PageHandler, LogEnabled { if (!run.isDisabled()) { run.setDisabled(true); m_abtestRunDao.updateByPK(run, AbtestRunEntity.UPDATESET_STATUS); + m_service.setModified(); } else { error.addArgument(id, String.format("Abtest %d has been already suspended!", id)); } @@ -231,6 +317,7 @@ public class Handler implements PageHandler, LogEnabled { if (run.isDisabled()) { run.setDisabled(false); m_abtestRunDao.updateByPK(run, AbtestRunEntity.UPDATESET_STATUS); + m_service.setModified(); } else { error.addArgument(id, String.format("Abtest %d has been already active!", id)); } @@ -250,32 +337,26 @@ public class Handler implements PageHandler, LogEnabled { } } - private void handleUpdateAction(Context ctx, Payload payload) { - try { - AbtestRun run = new AbtestRun(); - - run.setId(payload.getId()); - run.setKeyId(payload.getId()); - run.setCreator(payload.getOwner()); - run.setStartDate(payload.getStartDate()); - run.setEndDate(payload.getEndDate()); - run.setDomains(StringUtils.join(payload.getDomains(), ',')); - run.setStrategyConfiguration(payload.getStrategyConfig()); - Date now = new Date(); - run.setModifiedDate(now); + @Override + public void initialize() throws InitializationException { + if (m_service instanceof Task) { + Threads.forGroup("Cat").start((Task) m_service); + } - // only update run info, do not update abtest meta-info - m_abtestRunDao.updateByPK(run, AbtestRunEntity.UPDATESET_ALLOWED_MODIFYPART); - } catch (DalException e) { - m_logger.error("Error when updating abtest", e); + m_configuration = new Configuration(); + m_configuration.setDefaultEncoding("UTF-8"); + try { + m_configuration.setClassForTemplateLoading(ScriptFragementTest.class, "/freemaker"); + } catch (Exception e) { Cat.logError(e); - ctx.setException(e); } + + m_urlScriptProvider = new URLScriptProvider(); } private void renderCreateModel(Model model) { - Map> projectMap = getAllProjects(); - List groupStrategyList = getAllGroupStrategys(); + Map> projectMap = m_service.getAllProjects(); + List groupStrategyList = m_service.getAllGroupStrategies(); model.setProjectMap(projectMap); model.setGroupStrategyList(groupStrategyList); @@ -283,148 +364,90 @@ public class Handler implements PageHandler, LogEnabled { private void renderDetailModel(Context ctx, Model model, Payload payload) { renderCreateModel(model); - renderReportModel(ctx, model, payload); - } - - private void renderListModel(Model model, Payload payload) { - List reports = new ArrayList(); - AbtestStatus status = AbtestStatus.getByName(payload.getStatus(), null); - Date now = new Date(); - - List filterReports = new ArrayList(); - List totalReports = new ArrayList(); - int createdCount = 0, readyCount = 0, runningCount = 0, terminatedCount = 0, suspendedCount = 0; - - List runs = new ArrayList(); + int runId = payload.getId(); try { - runs = m_abtestRunDao.findAll(AbtestRunEntity.READSET_FULL); + AbtestRun run = m_abtestRunDao.findByPK(runId, AbtestRunEntity.READSET_FULL); + Abtest abtest = m_abtestDao.findByPK(run.getCaseId(), AbtestEntity.READSET_FULL); - for (AbtestRun run : runs) { - Abtest abtest = m_abtestDao.findByPK(run.getCaseId(), AbtestEntity.READSET_FULL); - ABTestReport report = new ABTestReport(abtest, run, now); + AbtestItem item = new AbtestItem(abtest, run); - totalReports.add(report); - if (status != null && report.getStatus() == status) { - filterReports.add(report); - } - switch (report.getStatus()) { - case CREATED: - createdCount++; - break; - case READY: - readyCount++; - break; - case RUNNING: - runningCount++; - break; - case TERMINATED: - terminatedCount++; - break; - case SUSPENDED: - suspendedCount++; - break; - } - } - } catch (Throwable e) { + model.setAbtest(item); + } catch (DalException e) { Cat.logError(e); + m_logger.error("Error when fetching abtest", e); + ctx.setException(e); } + } - model.setCreatedCount(createdCount); - model.setReadyCount(readyCount); - model.setRunningCount(runningCount); - model.setTerminatedCount(terminatedCount); - model.setSuspendedCount(suspendedCount); + private void renderModel(Model model, Payload payload) { + long lastUpdateTime = payload.getLastUpdateTime(); + AbtestModel filteredModel = new AbtestModel(); - if (status != null) { - totalReports = null; - totalReports = filterReports; - } + if (lastUpdateTime < m_service.getModifiedTime()) { + AbtestModel abtestModel = m_service.getAbtestModelByStatus(AbtestStatus.READY, AbtestStatus.RUNNING); - int totalSize = totalReports.size(); - int totalPages = totalSize % m_pageSize == 0 ? (totalSize / m_pageSize) : (totalSize / m_pageSize + 1); + for (Case _case : abtestModel.getCases()) { + Case newCase = new Case(); - // safe guarder for pageNum - if (payload.getPageNum() >= totalPages) { - if (totalPages == 0) { - payload.setPageNum(1); - } else { - payload.setPageNum(totalPages); - } - } else if (payload.getPageNum() <= 0) { - payload.setPageNum(1); - } + for (Run run : _case.getRuns()) { + if (run.getLastModifiedDate().getTime() > lastUpdateTime) { + newCase.addRun(run); + } + } - int fromIndex = (payload.getPageNum() - 1) * m_pageSize; - int toIndex = (fromIndex + m_pageSize) <= totalSize ? (fromIndex + m_pageSize) : totalSize; - for (int i = fromIndex; i < toIndex; i++) { - reports.add(totalReports.get(i)); + if (newCase.getRuns().size() > 0) { + newCase.setId(_case.getId()); + newCase.setGroupStrategy(_case.getGroupStrategy()); + newCase.setOwner(_case.getOwner()); + newCase.setDescription(_case.getDescription()); + newCase.getDomains().addAll(_case.getDomains()); + newCase.mergeAttributes(_case); + + filteredModel.addCase(newCase); + } + } } - model.setTotalPages(totalPages); - model.setDate(now); - model.setReports(reports); + model.setAbtestModel(filteredModel); } - private void renderModel(Model model) { - model.setAbtestModel(String.valueOf(fetchAbtestModel())); + /** + * + * @param code + * 0 for success, 1 for failure + * @param msg + * @return + */ + public String responseJson(int code, String msg) { + Gson gson = m_gsonBuilderManager.getGsonBuilder().create(); + return gson.toJson(new ResponseJson(code, msg), ResponseJson.class); } - private void renderReportModel(Context ctx, Model model, Payload payload) { - try { - int runId = payload.getId(); - AbtestRun run = m_abtestRunDao.findByPK(runId, AbtestRunEntity.READSET_FULL); - Abtest abtest = m_abtestDao.findByPK(run.getCaseId(), AbtestEntity.READSET_FULL); - AbtestDaoModel abtestModel = new AbtestDaoModel(abtest, run); + static class ResponseJson { + private int m_code; - model.setAbtest(abtestModel); - } catch (DalException e) { - Cat.logError(e); - m_logger.error("Error when fetching abtest", e); - ctx.setException(e); - } - } + private String m_msg; - private Case transform(AbtestRun abtestRun, Abtest entity, GroupStrategy groupStrategy) throws DalException { - Case _case = new Case(entity.getId()); - _case.setCreatedDate(entity.getCreationDate()); - _case.setDescription(entity.getDescription()); - _case.setGroupStrategy(groupStrategy.getName()); - _case.setName(entity.getName()); - _case.setOwner(entity.getOwner()); - _case.setLastModifiedDate(entity.getModifiedDate()); - for (String domain : StringUtils.split(entity.getDomains(), ',')) { - _case.addDomain(domain); + public ResponseJson(int code, String msg) { + m_code = code; + m_msg = msg; } - Run run = new Run(abtestRun.getId()); - for (String domain : StringUtils.split(abtestRun.getDomains(), ',')) { - run.addDomain(domain); + public int getCode() { + return m_code; } - run.setCreator(abtestRun.getCreator()); - run.setDisabled(false); - run.setEndDate(abtestRun.getEndDate()); - run.setGroupStrategyConfiguration(abtestRun.getStrategyConfiguration()); - run.setStartDate(abtestRun.getStartDate()); - _case.addRun(run); + public void setCode(int code) { + m_code = code; + } - return _case; - } + public String getMsg() { + return m_msg; + } - private Map> transform(List projects) { - Map> re = new TreeMap>(); - if (projects != null) { - for (Project project : projects) { - String key = project.getDepartment() + "-" + project.getProjectLine(); - List list = re.get(key); - if (list == null) { - list = new ArrayList(); - re.put(key, list); - } - list.add(project); - } + public void setMsg(String msg) { + m_msg = msg; } - return re; } } diff --git a/cat-home/src/main/java/com/dianping/cat/system/page/abtest/JspFile.java b/cat-home/src/main/java/com/dianping/cat/system/page/abtest/JspFile.java index 4f6fd1c6f576673102c2c22c4c86513f39e8f560..751ae483642986bc8773036abd73a556754f1686 100644 --- a/cat-home/src/main/java/com/dianping/cat/system/page/abtest/JspFile.java +++ b/cat-home/src/main/java/com/dianping/cat/system/page/abtest/JspFile.java @@ -2,16 +2,20 @@ package com.dianping.cat.system.page.abtest; public enum JspFile { + VIEW("/jsp/system/abtest/abtestAllTest.jsp"), + CREATE("/jsp/system/abtest/abtestCreate.jsp"), DETAIL("/jsp/system/abtest/abtestDetail.jsp"), - VIEW("/jsp/system/abtest/abtestAllTest.jsp"), - REPORT("/jsp/system/abtest/abtestReport.jsp"), - MODEL("/jsp/system/abtest/abtestModel.jsp"); + MODEL("/jsp/system/abtest/abtestModel.jsp"), + AJAX("/jsp/system/abtest/abtestAjax.jsp"), + + ABTEST_CACULATOR("/jsp/system/abtest/abtestCaculator.jsp"); + private String m_path; private JspFile(String path) { diff --git a/cat-home/src/main/java/com/dianping/cat/system/page/abtest/JspViewer.java b/cat-home/src/main/java/com/dianping/cat/system/page/abtest/JspViewer.java index 15e8e86bc24d05d86416533a78bb98e493d68358..68113dda3b640256efa954a72dfc2ac55d4643e8 100644 --- a/cat-home/src/main/java/com/dianping/cat/system/page/abtest/JspViewer.java +++ b/cat-home/src/main/java/com/dianping/cat/system/page/abtest/JspViewer.java @@ -14,13 +14,22 @@ public class JspViewer extends BaseJspViewer return JspFile.VIEW.getPath(); case CREATE: return JspFile.CREATE.getPath(); + case AJAX_ADDGROUPSTRATEGY: + return JspFile.AJAX.getPath(); + case AJAX_PARSEGROUPSTRATEGY: + return JspFile.AJAX.getPath(); + case AJAX_CREATE: + return JspFile.AJAX.getPath(); + case AJAX_DETAIL: + return JspFile.AJAX.getPath(); case DETAIL: return JspFile.DETAIL.getPath(); case REPORT: return JspFile.REPORT.getPath(); case MODEL: return JspFile.MODEL.getPath(); - + case ABTEST_CACULATOR: + return JspFile.ABTEST_CACULATOR.getPath(); } throw new RuntimeException("Unknown action: " + action); diff --git a/cat-home/src/main/java/com/dianping/cat/system/page/abtest/ListViewHandler.java b/cat-home/src/main/java/com/dianping/cat/system/page/abtest/ListViewHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..5ba1babfc06dbbe8de77a0c1c3be238838d17204 --- /dev/null +++ b/cat-home/src/main/java/com/dianping/cat/system/page/abtest/ListViewHandler.java @@ -0,0 +1,118 @@ +package com.dianping.cat.system.page.abtest; + +import java.util.ArrayList; +import java.util.List; + +import org.unidal.lookup.annotation.Inject; + +import com.dianping.cat.Cat; +import com.dianping.cat.home.dal.abtest.Abtest; +import com.dianping.cat.home.dal.abtest.AbtestDao; +import com.dianping.cat.home.dal.abtest.AbtestEntity; +import com.dianping.cat.home.dal.abtest.AbtestRun; +import com.dianping.cat.home.dal.abtest.AbtestRunDao; +import com.dianping.cat.home.dal.abtest.AbtestRunEntity; +import com.dianping.cat.system.page.abtest.ListViewModel.AbtestItem; + +public class ListViewHandler implements SubHandler { + @Inject + private AbtestDao m_abtestDao; + + @Inject + private AbtestRunDao m_abtestRunDao; + + @Inject + private int m_pageSize = 10; + + @Override + public void handle(Context ctx, Model model, Payload payload) { + ListViewModel listViewModel = new ListViewModel(); + AbtestStatus status = AbtestStatus.getByName(payload.getStatus(), null); + + List filterItems = new ArrayList(); + List totalItems = new ArrayList(); + int createdCount = 0, readyCount = 0, runningCount = 0, terminatedCount = 0, suspendedCount = 0; + + List runs = new ArrayList(); + + try { + runs = m_abtestRunDao.findAll(AbtestRunEntity.READSET_FULL); + + for (AbtestRun run : runs) { + Abtest abtest = m_abtestDao.findByPK(run.getCaseId(), AbtestEntity.READSET_FULL); + AbtestItem item = new AbtestItem(abtest, run); + + totalItems.add(item); + + if (status != null && item.getStatus() == status) { + filterItems.add(item); + } + + switch (item.getStatus()) { + case CREATED: + createdCount++; + break; + case READY: + readyCount++; + break; + case RUNNING: + runningCount++; + break; + case TERMINATED: + terminatedCount++; + break; + case SUSPENDED: + suspendedCount++; + break; + } + } + } catch (Throwable e) { + Cat.logError(e); + } + + listViewModel.setCreatedCount(createdCount); + listViewModel.setReadyCount(readyCount); + listViewModel.setRunningCount(runningCount); + listViewModel.setTerminatedCount(terminatedCount); + listViewModel.setSuspendedCount(suspendedCount); + + if (status != null) { + totalItems = null; + totalItems = filterItems; + } + + int totalSize = totalItems.size(); + int totalPages = totalSize % m_pageSize == 0 ? (totalSize / m_pageSize) : (totalSize / m_pageSize + 1); + + // safe guarder for pageNum + if (payload.getPageNum() >= totalPages) { + if (totalPages == 0) { + payload.setPageNum(1); + } else { + payload.setPageNum(totalPages); + } + } else if (payload.getPageNum() <= 0) { + payload.setPageNum(1); + } + + int fromIndex = (payload.getPageNum() - 1) * m_pageSize; + int toIndex = (fromIndex + m_pageSize) <= totalSize ? (fromIndex + m_pageSize) : totalSize; + + listViewModel.setTotalPages(totalPages); + listViewModel.setItems(totalItems.subList(fromIndex, toIndex)); + + model.setListViewModel(listViewModel); + } + + public void setAbtestDao(AbtestDao abtestDao) { + m_abtestDao = abtestDao; + } + + public void setAbtestRunDao(AbtestRunDao abtestRunDao) { + m_abtestRunDao = abtestRunDao; + } + + public void setPageSize(int pageSize) { + m_pageSize = pageSize; + } +} diff --git a/cat-home/src/main/java/com/dianping/cat/system/page/abtest/ListViewModel.java b/cat-home/src/main/java/com/dianping/cat/system/page/abtest/ListViewModel.java new file mode 100644 index 0000000000000000000000000000000000000000..638d1651ca4f83ece753c798146e741544da8a56 --- /dev/null +++ b/cat-home/src/main/java/com/dianping/cat/system/page/abtest/ListViewModel.java @@ -0,0 +1,160 @@ +package com.dianping.cat.system.page.abtest; + +import java.util.Date; +import java.util.List; + +import com.dianping.cat.home.dal.abtest.Abtest; +import com.dianping.cat.home.dal.abtest.AbtestRun; + +public class ListViewModel { + + private int m_totalPages; + + private int m_createdCount; + + private int m_readyCount; + + private int m_runningCount; + + private int m_terminatedCount; + + private int m_suspendedCount; + + private List m_items; + + public int getCreatedCount() { + return m_createdCount; + } + + public List getItems() { + return m_items; + } + + public int getReadyCount() { + return m_readyCount; + } + + public int getRunningCount() { + return m_runningCount; + } + + public int getSuspendedCount() { + return m_suspendedCount; + } + + public int getTerminatedCount() { + return m_terminatedCount; + } + + public int getTotalPages() { + return m_totalPages; + } + + public void setCreatedCount(int createdCount) { + m_createdCount = createdCount; + } + + public void setItems(List item) { + m_items = item; + } + + public void setReadyCount(int readyCount) { + m_readyCount = readyCount; + } + + public void setRunningCount(int runningCount) { + m_runningCount = runningCount; + } + + public void setSuspendedCount(int suspendedCount) { + m_suspendedCount = suspendedCount; + } + + public void setTerminatedCount(int terminatedCount) { + m_terminatedCount = terminatedCount; + } + + public void setTotalPages(int totalPages) { + m_totalPages = totalPages; + } + + public static class AbtestItem { + + private Abtest m_abtest; + + private AbtestRun m_run; + + public AbtestItem(Abtest abtest, AbtestRun run) { + m_abtest = abtest; + m_run = run; + } + + public Abtest getAbtest() { + return m_abtest; + } + + public int getCaseId() { + return m_run.getCaseId(); + } + + public String getConditions() { + return m_run.getConditions(); + } + + public String getConversionGoals() { + return m_run.getConversionGoals(); + } + + public String getDescription() { + return m_abtest.getDescription(); + } + + public String getDomains() { + return m_run.getDomains(); + } + + public Date getEndDate() { + return m_run.getEndDate(); + } + + public int getGroupStrategy() { + return m_abtest.getGroupStrategy(); + } + + public int getId() { + return m_run.getId(); + } + + public String getName() { + return m_abtest.getName(); + } + + public String getOwner() { + return m_abtest.getOwner(); + } + + public AbtestRun getRun() { + return m_run; + } + + public Date getStartDate() { + return m_run.getStartDate(); + } + + public AbtestStatus getStatus() { + return AbtestStatus.calculateStatus(m_run, new Date()); + } + + public String getStrategyConfiguration() { + return m_run.getStrategyConfiguration(); + } + + public void setAbtest(Abtest abtest) { + m_abtest = abtest; + } + + public void setRun(AbtestRun run) { + m_run = run; + } + } +} diff --git a/cat-home/src/main/java/com/dianping/cat/system/page/abtest/Model.java b/cat-home/src/main/java/com/dianping/cat/system/page/abtest/Model.java index 18132ceed705471b767ef5c90cfba06c53ab42ae..828eb5624b69ebf2057a0f8fcef504ab41bb4823 100644 --- a/cat-home/src/main/java/com/dianping/cat/system/page/abtest/Model.java +++ b/cat-home/src/main/java/com/dianping/cat/system/page/abtest/Model.java @@ -6,12 +6,15 @@ import java.util.Map; import org.unidal.web.mvc.ViewModel; +import com.dianping.cat.abtest.model.entity.AbtestModel; import com.dianping.cat.abtest.spi.ABTestEntity; +import com.dianping.cat.advanced.metric.config.entity.MetricItemConfig; import com.dianping.cat.core.dal.Project; -import com.dianping.cat.home.dal.abtest.Abtest; -import com.dianping.cat.home.dal.abtest.AbtestRun; import com.dianping.cat.home.dal.abtest.GroupStrategy; +import com.dianping.cat.report.abtest.entity.AbtestReport; import com.dianping.cat.system.SystemPage; +import com.dianping.cat.system.page.abtest.ListViewModel.AbtestItem; +import com.dianping.cat.system.page.abtest.ReportHandler.DataSets; public class Model extends ViewModel { private String m_domain; @@ -20,27 +23,21 @@ public class Model extends ViewModel { private ABTestEntity m_entity; - private List m_reports; + private ListViewModel m_listViewModel; - private int m_totalPages; - - private int m_createdCount; - - private int m_readyCount; - - private int m_runningCount; + private Map> m_projectMap; - private int m_terminatedCount; + private Map m_metricConfigItem; - private int m_suspendedCount; + private List m_groupStrategyList; - private Map> m_projectMap; + private AbtestItem m_abtest; - private List m_groupStrategyList; + private AbtestModel m_abtestModel; - private AbtestDaoModel m_abtest; + private AbtestReport m_report; - private String m_abtestModel; + private List m_dataSets; private String m_ipAddress; @@ -48,16 +45,16 @@ public class Model extends ViewModel { super(ctx); } - public AbtestDaoModel getAbtest() { + public AbtestItem getAbtest() { return m_abtest; } - public String getAbtestModel() { + public AbtestModel getAbtestModel() { return m_abtestModel; } - public int getCreatedCount() { - return m_createdCount; + public List getDataSets() { + return m_dataSets; } public Date getDate() { @@ -85,48 +82,36 @@ public class Model extends ViewModel { return m_ipAddress; } - public Map> getProjectMap() { - return m_projectMap; - } - - public int getReadyCount() { - return m_readyCount; - } - - public List getReports() { - return m_reports; - } - - public String getReportType(){ - return ""; + public ListViewModel getListViewModel() { + return m_listViewModel; } - public int getRunningCount() { - return m_runningCount; + public Map getMetricConfigItem() { + return m_metricConfigItem; } - public int getSuspendedCount() { - return m_suspendedCount; + public Map> getProjectMap() { + return m_projectMap; } - public int getTerminatedCount() { - return m_terminatedCount; + public AbtestReport getReport() { + return m_report; } - public int getTotalPages() { - return m_totalPages; + public String getReportType() { + return ""; } - public void setAbtest(AbtestDaoModel abtest) { + public void setAbtest(AbtestItem abtest) { m_abtest = abtest; } - public void setAbtestModel(String abtestModel) { + public void setAbtestModel(AbtestModel abtestModel) { m_abtestModel = abtestModel; } - public void setCreatedCount(int createdCount) { - m_createdCount = createdCount; + public void setDataSets(List dataSets) { + m_dataSets = dataSets; } public void setDate(Date date) { @@ -149,100 +134,19 @@ public class Model extends ViewModel { m_ipAddress = ipAddress; } - public void setProjectMap(Map> projectMap) { - m_projectMap = projectMap; - } - - public void setReadyCount(int readyCount) { - m_readyCount = readyCount; + public void setListViewModel(ListViewModel listViewModel) { + m_listViewModel = listViewModel; } - public void setReports(List reports) { - m_reports = reports; + public void setMetricConfigItem(Map metricItemConfig) { + m_metricConfigItem = metricItemConfig; } - public void setRunningCount(int runningCount) { - m_runningCount = runningCount; - } - - public void setSuspendedCount(int suspendedCount) { - m_suspendedCount = suspendedCount; - } - - public void setTerminatedCount(int terminatedCount) { - m_terminatedCount = terminatedCount; - } - - public void setTotalPages(int totalPages) { - m_totalPages = totalPages; + public void setProjectMap(Map> projectMap) { + m_projectMap = projectMap; } - public static class AbtestDaoModel { - - private Abtest m_abtest; - - private AbtestRun m_run; - - public AbtestDaoModel(Abtest abtest, AbtestRun abtestRun) { - super(); - m_abtest = abtest; - m_run = abtestRun; - } - - public Abtest getAbtest() { - return m_abtest; - } - - public int getCaseId() { - return m_run.getCaseId(); - } - - public String getDescription() { - return m_abtest.getDescription(); - } - - public String getDomains() { - return m_run.getDomains(); - } - - public Date getEndDate() { - return m_run.getEndDate(); - } - - public int getGroupStrategy() { - return m_abtest.getGroupStrategy(); - } - - public int getId() { - return m_run.getId(); - } - - public String getName() { - return m_abtest.getName(); - } - - public String getOwner() { - return m_abtest.getOwner(); - } - - public AbtestRun getRun() { - return m_run; - } - - public Date getStartDate() { - return m_run.getStartDate(); - } - - public String getStrategyConfiguration() { - return m_run.getStrategyConfiguration(); - } - - public void setAbtest(Abtest abtest) { - m_abtest = abtest; - } - - public void setRun(AbtestRun run) { - m_run = run; - } + public void setReport(AbtestReport report) { + m_report = report; } } diff --git a/cat-home/src/main/java/com/dianping/cat/system/page/abtest/Payload.java b/cat-home/src/main/java/com/dianping/cat/system/page/abtest/Payload.java index 0803acdfef5254aca465da9da05f59c5566414fa..2ddd543750a1fae0c5b200b699fd35a5868736da 100644 --- a/cat-home/src/main/java/com/dianping/cat/system/page/abtest/Payload.java +++ b/cat-home/src/main/java/com/dianping/cat/system/page/abtest/Payload.java @@ -4,8 +4,8 @@ import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; +import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.Validate; -import org.codehaus.plexus.util.StringUtils; import org.unidal.web.mvc.ActionContext; import org.unidal.web.mvc.ActionPayload; import org.unidal.web.mvc.payload.annotation.FieldMeta; @@ -19,18 +19,14 @@ public class Payload implements ActionPayload { @FieldMeta("op") private Action m_action; - @FieldMeta("status") - private String m_status; - - @FieldMeta("pageNum") - private int m_pageNum; + /* ===============Abtest================ */ @FieldMeta("name") private String m_name; @FieldMeta("owner") private String m_owner; - + @FieldMeta("description") private String m_description; @@ -43,11 +39,13 @@ public class Payload implements ActionPayload { @FieldMeta("domains") private String[] m_domains; - @FieldMeta("strategyId") - private int m_strategyId; + @FieldMeta("conditions") + private String m_conditions; - @FieldMeta("strategyConfig") - private String m_strategyConfig; + @FieldMeta("goals") + private String m_conversionGoals; + + /* ===============Abtest Controls================ */ @FieldMeta("enable") private boolean m_enableAbtest; @@ -58,50 +56,150 @@ public class Payload implements ActionPayload { @FieldMeta("ids") private String m_ids; + @FieldMeta("status") + private String m_status; + + @FieldMeta("pageNum") + private int m_pageNum; + @FieldMeta("id") private int id; + @FieldMeta("lastUpdateTime") + private long m_lastUpdateTime; + + /* ===============GroupStrategy================ */ + + @FieldMeta("strategyId") + private int m_strategyId; + + @FieldMeta("strategyConfig") + private String m_strategyConfig; + + @FieldMeta("groupStrategyName") + private String m_groupStrategyName; + + @FieldMeta("groupStrategyClassName") + private String m_groupStrategyClassName; + + @FieldMeta("groupStrategyFullName") + private String m_groupStrategyFullName; + + @FieldMeta("groupStrategyDescriptor") + private String m_groupStrategyDescriptor; + + @FieldMeta("groupStrategyDescription") + private String m_groupStrategyDescription; + + @FieldMeta("srcCode") + private String m_srcCode; + + /* ===============Caculator================ */ + @FieldMeta("pv") + private int m_pv = 0; + + @FieldMeta("conversionRate") + private int m_conversionRate; + + /* ===============Report================ */ + @FieldMeta("selectMetricType") + private String m_selectMetricType; + + @FieldMeta("period") + private String m_period; + + private boolean m_addGs; + private String m_startDateStr; - + private String m_endDateStr; - - private SimpleDateFormat m_sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm"); - - public void setAction(String action) { - if (action.equalsIgnoreCase(Action.REPORT.getName())) { - m_action = Action.getByName(action, Action.REPORT); - } else if (action.equalsIgnoreCase(Action.CREATE.getName())) { - m_action = Action.getByName(action, Action.CREATE); - } else { - m_action = Action.getByName(action, Action.VIEW); - } - } + + private SimpleDateFormat m_dataFormater = new SimpleDateFormat("yyyy-MM-dd hh:mm"); @Override public Action getAction() { return m_action; } - public String getStatus() { - return m_status; + public boolean getAddGs() { + return m_addGs; } - public void setStatus(String status) { - AbtestStatus abstatus = AbtestStatus.getByName(status, null); + public String getConditions() { + return m_conditions; + } - if (abstatus != null) { - m_status = abstatus.name().toLowerCase(); + public String getConversionGoals() { + return m_conversionGoals; + } + + public int getConversionRate() { + return m_conversionRate; + } + + public String getDescription() { + return m_description; + } + + public int getDisableAbtest() { + return m_disableAbtest; + } + + public String[] getDomains() { + return m_domains; + } + + public Date getEndDate() { + return m_endDate; + } + + public String getEndDateStr() { + return m_endDateStr; + } + + public String getGroupStrategyClassName() { + return m_groupStrategyClassName; + } + + public String getGroupStrategyDescription() { + return m_groupStrategyDescription; + } + + public String getGroupStrategyDescriptor() { + return m_groupStrategyDescriptor; + } + + public String getGroupStrategyFullName() { + return m_groupStrategyFullName; + } + + public String getGroupStrategyName() { + return m_groupStrategyName; + } + + public int getId() { + return id; + } + + public String[] getIds() { + if (m_ids != null) { + String[] ids = m_ids.split("-"); + return ids; } else { - m_status = "all"; + return null; } } - public int getPageNum() { - return m_pageNum; + public long getLastUpdateTime() { + return m_lastUpdateTime; } - public void setPageNum(int pageNum) { - m_pageNum = pageNum; + public String getName() { + return m_name; + } + + public String getOwner() { + return m_owner; } @Override @@ -109,128 +207,199 @@ public class Payload implements ActionPayload { return m_page; } + public int getPageNum() { + return m_pageNum; + } + + public String getPeriod() { + return m_period; + } + + public int getPv() { + return m_pv; + } + public String getReportType() { return ""; } - @Override - public void setPage(String page) { - m_page = SystemPage.getByName(page, SystemPage.ABTEST); + public String getSelectMetricType() { + return m_selectMetricType; } - public String getName() { - return m_name; + public String getSrcCode() { + return m_srcCode; } - public void setName(String name) { - this.m_name = name; + public Date getStartDate() { + return m_startDate; } - - public String getOwner() { - return m_owner; - } - public void setOwner(String owner) { - m_owner = owner; - } + public String getStartDateStr() { + return m_startDateStr; + } - public String getDescription() { - return m_description; + public String getStatus() { + return m_status; } - public void setDescription(String description) { - this.m_description = description; + public String getStrategyConfig() { + return m_strategyConfig; } - public Date getStartDate() { - return m_startDate; + public int getStrategyId() { + return m_strategyId; } - public Date getEndDate() { - return m_endDate; + public boolean isEnableAbtest() { + return m_enableAbtest; } - public void setStartDate(String startDate) { - try { - m_startDateStr = startDate; - m_startDate = m_sdf.parse(startDate); - } catch (ParseException e) { - Cat.logError(e); + public void setAction(String action) { + if (action.equalsIgnoreCase(Action.REPORT.getName())) { + m_action = Action.getByName(action, Action.REPORT); + } else if (action.equalsIgnoreCase(Action.CREATE.getName())) { + m_action = Action.getByName(action, Action.CREATE); + } else { + m_action = Action.getByName(action, Action.VIEW); } } + public void setAddGs(boolean addGs) { + m_addGs = addGs; + } + + public void setConditions(String conditions) { + m_conditions = conditions; + } + + public void setConversionRate(int conversionRate) { + m_conversionRate = conversionRate; + } + + public void setDescription(String description) { + this.m_description = description; + } + + public void setDisableAbtest(int disableAbtest) { + m_disableAbtest = disableAbtest; + } + + public void setDomains(String[] domains) { + this.m_domains = domains; + } + public void setEndDate(String endDate) { try { m_endDateStr = endDate; - m_endDate = m_sdf.parse(endDate); + m_endDate = m_dataFormater.parse(endDate); } catch (ParseException e) { Cat.logError(e); } } - public String getStartDateStr() { - return m_startDateStr; + public void setEndDate2(Date endDate) { + m_endDate = endDate; } - public String getEndDateStr() { - return m_endDateStr; + public void setGoals(String goals) { + m_conversionGoals = goals; } - public String[] getDomains() { - return m_domains; + public void setGroupStrategyClassName(String groupStrategyClassName) { + m_groupStrategyClassName = groupStrategyClassName; } - public void setDomains(String[] domains) { - this.m_domains = domains; + public void setGroupStrategyDescription(String groupStrategyDescription) { + m_groupStrategyDescription = groupStrategyDescription; } - public int getStrategyId() { - return m_strategyId; + public void setGroupStrategyDescriptor(String groupStrategyDescriptor) { + m_groupStrategyDescriptor = groupStrategyDescriptor; } - public void setStrategyId(int strategyId) { - this.m_strategyId = strategyId; + public void setGroupStrategyFullName(String groupStrategyFullName) { + m_groupStrategyFullName = groupStrategyFullName; } - public String getStrategyConfig() { - return m_strategyConfig; + public void setGroupStrategyName(String groupStrategyName) { + m_groupStrategyName = groupStrategyName; } - public void setStrategyConfig(String strategyConfig) { - this.m_strategyConfig = strategyConfig; + public void setId(int id) { + this.id = id; } - public boolean isEnableAbtest() { - return m_enableAbtest; + public void setIds(String ids) { + m_ids = ids; } - public int getDisableAbtest() { - return m_disableAbtest; + public void setLastUpdateTime(long lastUpdateTime) { + m_lastUpdateTime = lastUpdateTime; } - public void setDisableAbtest(int disableAbtest) { - m_disableAbtest = disableAbtest; + public void setName(String name) { + this.m_name = name; } - public String[] getIds() { - if (m_ids != null) { - String[] ids = m_ids.split("-"); - return ids; - } else { - return null; + public void setOwner(String owner) { + m_owner = owner; + } + + @Override + public void setPage(String page) { + m_page = SystemPage.getByName(page, SystemPage.ABTEST); + } + + public void setPageNum(int pageNum) { + m_pageNum = pageNum; + } + + public void setPeriod(String period) { + m_period = period; + } + + public void setPv(int pv) { + m_pv = pv; + } + + public void setSelectMetricType(String selectMetricType) { + m_selectMetricType = selectMetricType; + } + + public void setSrcCode(String srcCode) { + m_srcCode = srcCode; + } + + public void setStartDate(String startDate) { + try { + m_startDateStr = startDate; + m_startDate = m_dataFormater.parse(startDate); + } catch (ParseException e) { + Cat.logError(e); } } - public void setIds(String ids) { - m_ids = ids; + public void setStartDate2(Date startDate) { + m_startDate = startDate; } - public int getId() { - return id; + public void setStatus(String status) { + AbtestStatus abstatus = AbtestStatus.getByName(status, null); + + if (abstatus != null) { + m_status = abstatus.name().toLowerCase(); + } else { + m_status = "all"; + } } - public void setId(int id) { - this.id = id; + public void setStrategyConfig(String strategyConfig) { + this.m_strategyConfig = strategyConfig; + } + + public void setStrategyId(int strategyId) { + this.m_strategyId = strategyId; } @Override @@ -238,27 +407,36 @@ public class Payload implements ActionPayload { if (m_action == null) { m_action = Action.VIEW; } - // 验证doCreate的参数 - if (m_action == Action.CREATE && ctx.getHttpServletRequest().getMethod().equalsIgnoreCase("post")) { - try { - Validate.isTrue(StringUtils.isNotBlank(m_name), "'ABTest Name' is required"); -// Validate.isTrue(m_startDate != null, "'Start Time' is required, and formated 'yyyy-MM-dd hh:mm'"); -// Validate.isTrue(m_endDate != null, "'End Time' is required, and formated 'yyyy-MM-dd hh:mm'"); - Validate.isTrue(m_domains != null && m_domains.length > 0, "'Domains' is required, choose one at least"); - for (String domain : m_domains) { - Validate.isTrue(StringUtils.isNotBlank(domain), "'Domains' should not be blank"); - } - Validate.isTrue(m_strategyId > 0, "'Strategy' is required, choose one at least"); - } catch (IllegalArgumentException e) { - ctx.setException(e); - } - } + if (m_status == null) { m_status = "all"; } + if (m_disableAbtest != -1 && m_disableAbtest != 1) { m_disableAbtest = 0; } + if (ctx.getHttpServletRequest().getMethod().equalsIgnoreCase("post")) { + if (m_action == Action.CREATE) { + try { + Validate.isTrue(StringUtils.isNotBlank(m_name), "'ABTest Name' is required"); + Validate.isTrue(m_domains != null && m_domains.length > 0, "'Domains' is required, choose one at least"); + for (String domain : m_domains) { + Validate.isTrue(StringUtils.isNotBlank(domain), "'Domains' should not be blank"); + } + Validate.isTrue(m_strategyId > 0, "'Strategy' is required, choose one at least"); + } catch (IllegalArgumentException e) { + ctx.setException(e); + } + } else if (m_action == Action.AJAX_ADDGROUPSTRATEGY) { + try { + Validate.isTrue(StringUtils.isNotBlank(m_groupStrategyName), "'GroupStrategy Name' is required"); + Validate.isTrue(StringUtils.isNotBlank(m_groupStrategyClassName), + "'GroupStrategy ClassName' is required"); + } catch (IllegalArgumentException e) { + ctx.setException(e); + } + } + } } } diff --git a/cat-home/src/main/java/com/dianping/cat/system/page/abtest/ReportHandler.java b/cat-home/src/main/java/com/dianping/cat/system/page/abtest/ReportHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..195161ff7212667273f8dc0a291468ea23fc073e --- /dev/null +++ b/cat-home/src/main/java/com/dianping/cat/system/page/abtest/ReportHandler.java @@ -0,0 +1,593 @@ +package com.dianping.cat.system.page.abtest; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.codehaus.plexus.component.repository.exception.ComponentLookupException; +import org.codehaus.plexus.personality.plexus.lifecycle.phase.Initializable; +import org.codehaus.plexus.personality.plexus.lifecycle.phase.InitializationException; +import org.unidal.lookup.ContainerLoader; +import org.unidal.lookup.annotation.Inject; + +import com.dianping.cat.Cat; +import com.dianping.cat.advanced.metric.config.entity.MetricItemConfig; +import com.dianping.cat.consumer.advanced.MetricConfigManager; +import com.dianping.cat.home.dal.abtest.Abtest; +import com.dianping.cat.home.dal.abtest.AbtestDao; +import com.dianping.cat.home.dal.abtest.AbtestEntity; +import com.dianping.cat.home.dal.abtest.AbtestReportDao; +import com.dianping.cat.home.dal.abtest.AbtestReportEntity; +import com.dianping.cat.home.dal.abtest.AbtestRun; +import com.dianping.cat.home.dal.abtest.AbtestRunDao; +import com.dianping.cat.home.dal.abtest.AbtestRunEntity; +import com.dianping.cat.report.abtest.entity.AbtestReport; +import com.dianping.cat.report.abtest.entity.Chart; +import com.dianping.cat.report.abtest.entity.Goal; +import com.dianping.cat.report.abtest.entity.Variation; +import com.dianping.cat.report.abtest.transform.BaseVisitor; +import com.dianping.cat.report.abtest.transform.DefaultSaxParser; +import com.dianping.cat.report.task.abtest.ABTestReportBuilder; +import com.dianping.cat.system.page.abtest.ListViewModel.AbtestItem; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; + +public class ReportHandler implements SubHandler, Initializable { + @Inject + private AbtestDao m_abtestDao; + + @Inject + private AbtestRunDao m_abtestRunDao; + + @Inject + private AbtestReportDao m_abtestReportDao; + + @Inject + private MetricConfigManager m_configManager; + + private static GsonBuilderManager m_gsonBuilderManager = new GsonBuilderManager(); + + private Calendar m_calendar = Calendar.getInstance(); + + private DateFormat m_dateFormatForHour = new SimpleDateFormat("MM-dd HH:00"); + + private DateFormat m_dateFormatForDay = new SimpleDateFormat("MM-dd"); + + private AbtestReport buildDailyReport(AbtestReport query, String goal, Model model) { + Date startTime = query.getStartTime(); + Date endTime = query.getEndTime(); + m_calendar.setTime(startTime); + + long deltaTime = endTime.getTime() - startTime.getTime(); + long day = deltaTime / (24 * 60 * 60 * 1000L); + int step = ((int) day / 7 > 0) ? ((int) day / 7) : 1; + int count = 0; + List datas = new ArrayList(); + List labels = new ArrayList(); + + m_calendar.add(Calendar.DAY_OF_MONTH, 1); + endTime = m_calendar.getTime(); + + while (startTime.before(query.getEndTime())) { + if (count % step == 0) { + List reports = queryReport(query.getRunId(), startTime, endTime); + + AbtestReport report = mergeReport(reports); + + datas.add(report); + + labels.add(m_dateFormatForDay.format(endTime)); + } else { + labels.add(""); + } + + count++; + startTime = endTime; + m_calendar.add(Calendar.DAY_OF_MONTH, 1); + endTime = m_calendar.getTime(); + } + + AbtestReport report = mergeReport(datas); + Chart chart = new Chart(); + + if (goal.length() == 0) { + for (Goal _goal : report.getGoals()) { + goal = _goal.getName(); + + if (goal.length() > 0) { + break; + } + } + } + + String datasets = buildDateSets(datas, goal, report.getVariations().keySet(), model); + Gson gson = m_gsonBuilderManager.getGsonBuilder().create(); + + String label = gson.toJson(labels, new TypeToken>() { + }.getType()); + + chart.setType("day"); + chart.setLabels(label); + chart.setDatasets(datasets); + chart.setGoal(goal); + + report.setChart(chart); + report.setRunId(query.getRunId()); + report.setStartTime(query.getStartTime()); + report.setEndTime(query.getEndTime()); + + return report; + } + + private String buildDateSets(List reports, String goal, Set set, Model model) { + List dataSets = new ArrayList(); + + for (String key : set) { + List data = new ArrayList(); + + for (AbtestReport report : reports) { + if (report.getStartTime() != null) { + Variation variation = report.findVariation(key); + + if (variation != null) { + Goal tmp = variation.findGoal(goal); + + if (tmp != null) { + if (tmp.getType().equals("C")) { + data.add(tmp.getCount()); + } else if (tmp.getType().equals("S")) { + data.add(tmp.getSum()); + } else { + data.add(tmp.getAvg()); + } + } else { + data.add(0); + } + } else { + data.add(0); + } + } else { + data.add(0); + } + } + + DataSets dataSet = DataSetsBuilder.buildDataSets(DataSetColor.getDataSetColor(key), data); + dataSets.add(dataSet); + } + + model.setDataSets(dataSets); + Gson gson = m_gsonBuilderManager.getGsonBuilder().create(); + + return gson.toJson(dataSets, new TypeToken>() { + }.getType()); + } + + private AbtestReport buildHourlyReport(AbtestReport query, String goal, Model model) { + Date startTime = query.getStartTime(); + Date endTime = query.getEndTime(); + + List reports = queryReport(query.getRunId(), startTime, endTime); + List datas = new ArrayList(); + List labels = new ArrayList(); + + long deltaTime = endTime.getTime() - startTime.getTime(); + long hour = deltaTime / (60 * 60 * 1000L); + int step = ((int) hour / 12 > 0) ? ((int) hour / 12) : 1; + int size = reports.size(); + int count = 0; + int i = 0; + + m_calendar.setTime(startTime); + m_calendar.add(Calendar.HOUR, 1); + endTime = m_calendar.getTime(); + + while (startTime.before(query.getEndTime())) { + if (count % step == 0) { + labels.add(m_dateFormatForHour.format(startTime)); + } else { + labels.add(""); + } + + if (i < size) { + AbtestReport re = reports.get(i); + + if (re.getStartTime().equals(startTime)) { + datas.add(re); + + i++; + } else { + datas.add(new AbtestReport()); + } + } + + count++; + startTime = endTime; + m_calendar.add(Calendar.HOUR, 1); + endTime = m_calendar.getTime(); + } + + AbtestReport report = mergeReport(datas); + Chart chart = new Chart(); + + if (goal.length() == 0) { + for (Goal _goal : report.getGoals()) { + goal = _goal.getName(); + + if (goal.length() > 0) { + break; + } + } + } + + String datasets = buildDateSets(datas, goal, report.getVariations().keySet(), model); + Gson gson = m_gsonBuilderManager.getGsonBuilder().create(); + + String label = gson.toJson(labels, new TypeToken>() { + }.getType()); + + chart.setType("hour"); + chart.setLabels(label); + chart.setDatasets(datasets); + chart.setGoal(goal); + + report.setChart(chart); + report.setRunId(query.getRunId()); + report.setStartTime(query.getStartTime()); + report.setEndTime(query.getEndTime()); + + return report; + } + + private AbtestReport buildQuery(int runId, Date startTime, Date endTime, String period) { + AbtestReport query = new AbtestReport(); + Date now = new Date(); + Date newStartTime = null; + Date newEndTime = null; + + query.setRunId(runId); + + if (startTime == null && endTime == null) { + m_calendar.setTime(now); + + if (period.equals("day")) { + m_calendar.add(Calendar.DAY_OF_MONTH, -7); + } else { + m_calendar.add(Calendar.HOUR_OF_DAY, -24); + } + + newStartTime = m_calendar.getTime(); + newEndTime = now; + } else if (endTime == null) { + m_calendar.setTime(startTime); + + if (period.equals("day")) { + m_calendar.add(Calendar.DAY_OF_MONTH, 7); + } else { + m_calendar.add(Calendar.HOUR_OF_DAY, 24); + } + + newStartTime = startTime; + newEndTime = m_calendar.getTime(); + } else if (startTime == null) { + m_calendar.setTime(endTime); + + if (period.equals("day")) { + m_calendar.add(Calendar.DAY_OF_MONTH, -7); + } else { + m_calendar.add(Calendar.HOUR_OF_DAY, -24); + } + + newStartTime = m_calendar.getTime(); + newEndTime = endTime; + } else { + newStartTime = startTime; + newEndTime = endTime; + } + + query.setStartTime(resetTime(period, newStartTime)); + query.setEndTime(resetTime(period, newEndTime)); + + return query; + } + + public AbtestReport buildReport(AbtestReport query, String goal, String period, Model model) { + if (period.equals("day")) { + return buildDailyReport(query, goal, model); + } else { + return buildHourlyReport(query, goal, model); + } + } + + public Map getMetricItemConfig() { + return m_configManager.getMetricConfig().getMetricItemConfigs(); + } + + @Override + public void handle(Context ctx, Model model, Payload payload) { + int runId = payload.getId(); + try { + AbtestRun run = m_abtestRunDao.findByPK(runId, AbtestRunEntity.READSET_FULL); + Abtest abtest = m_abtestDao.findByPK(run.getCaseId(), AbtestEntity.READSET_FULL); + + AbtestItem item = new AbtestItem(abtest, run); + + Date startTime = payload.getStartDate(); + Date endTime = payload.getEndDate(); + String goal = payload.getSelectMetricType(); + String period = payload.getPeriod(); + + if (period == null || (!period.equals("hour") && !period.equals("day"))) { + period = "hour"; + } + + if (goal == null) { + goal = ""; + } + + AbtestReport query = buildQuery(runId, startTime, endTime, period); + AbtestReport report = buildReport(query, goal, period, model); + + Collections.sort(report.getGoals(), new Comparator() { + @Override + public int compare(Goal o1, Goal o2) { + Map metricItemConfig = getMetricItemConfig(); + + MetricItemConfig item1 = metricItemConfig.get(o1.getName()); + MetricItemConfig item2 = metricItemConfig.get(o2.getName()); + + if (item1.getViewOrder() > item2.getViewOrder()) { + return 1; + } else { + return -1; + } + } + }); + + model.setAbtest(item); + model.setReport(report); + model.setMetricConfigItem(getMetricItemConfig()); + + payload.setStartDate2(query.getStartTime()); + payload.setEndDate2(query.getEndTime()); + } catch (Exception e) { + Cat.logError(e); + e.printStackTrace(); + } + } + + @Override + public void initialize() throws InitializationException { + try { + ContainerLoader.getDefaultContainer().lookup(ABTestReportBuilder.class); + } catch (ComponentLookupException e) { + Cat.logError(e); + } + } + + private AbtestReport mergeReport(List reports) { + AbtestReport result = new AbtestReport(); + + AbtestReportVisitor visitor = new AbtestReportVisitor(result); + + for (AbtestReport report : reports) { + if (report.getStartTime() != null) { + visitor.visitAbtestReport(report); + + result.setRunId(report.getRunId()); + + if (result.getStartTime() == null || result.getEndTime() == null) { + result.setStartTime(report.getStartTime()); + result.setEndTime(report.getEndTime()); + } + + if (result.getStartTime().after(report.getStartTime())) { + result.setStartTime(report.getStartTime()); + } + + if (result.getEndTime().before(report.getEndTime())) { + result.setEndTime(report.getEndTime()); + } + } + } + + return result; + } + + private List queryReport(int runId, Date startTime, Date endTime) { + List results = new ArrayList(); + + try { + List reports = m_abtestReportDao.findByRunIdDuration(runId, + startTime, endTime, AbtestReportEntity.READSET_FULL); + + for (com.dianping.cat.home.dal.abtest.AbtestReport report : reports) { + String content = report.getContent(); + + AbtestReport result = DefaultSaxParser.parse(content); + results.add(result); + } + } catch (Exception e) { + Cat.logError(e); + } + + return results; + } + + private Date resetTime(String period, Date time) { + m_calendar.setTime(time); + m_calendar.set(Calendar.MINUTE, 0); + m_calendar.set(Calendar.SECOND, 0); + m_calendar.set(Calendar.MILLISECOND, 0); + + if (period.equals("day")) { + m_calendar.set(Calendar.HOUR_OF_DAY, 0); + } + + return m_calendar.getTime(); + } + + class AbtestReportVisitor extends BaseVisitor { + + private AbtestReport m_report; + + private String m_variation = ""; + + private Set m_variationSet; + + private Map m_metricItemConfig; + + public AbtestReportVisitor(AbtestReport report) { + m_report = report; + m_variationSet = new HashSet(); + + m_variationSet.add("Control"); + m_variationSet.add("A"); + m_variationSet.add("B"); + m_variationSet.add("C"); + + m_metricItemConfig = getMetricItemConfig(); + } + + public AbtestReport getReport() { + return m_report; + } + + @Override + public void visitAbtestReport(AbtestReport abtestReport) { + for (Goal goal : abtestReport.getGoals()) { + String name = goal.getName(); + MetricItemConfig tmp = m_metricItemConfig.get(name); + + if (tmp.getViewOrder() > 0) { + m_report.findOrCreateGoal(name); + } + } + + for (Variation variation : abtestReport.getVariations().values()) { + if (m_variationSet.contains(variation.getName())) { + visitVariation(variation); + } + } + } + + @Override + public void visitGoal(Goal goal) { + String name = goal.getName(); + + if (m_variation != null && m_variation.length() > 0) { + Variation variation = m_report.findOrCreateVariation(m_variation); + + Goal result = variation.findOrCreateGoal(name); + + result.setType(goal.getType()); + result.setCount(result.getCount() + goal.getCount()); + result.setSum(result.getSum() + goal.getSum()); + // avg? + } + } + + @Override + public void visitVariation(Variation variation) { + m_variation = variation.getName(); + + if (m_variation != null && m_variation.length() > 0) { + m_report.findOrCreateVariation(m_variation); + + for (Goal goal : variation.getGoals().values()) { + visitGoal(goal); + } + } + } + } + + enum DataSetColor { + CONTROL(new DataSets("rgba(70, 136, 71,0.2)", "rgba(70, 136, 71,1)", "rgba(70, 136, 71, 1)", "#468847", null)), + + A(new DataSets("rgba(58, 135, 173, 0.3)", "rgba(58, 135, 173, 1)", "rgba(58, 135, 173, 1)", "#3a87ad", null)), + + B(new DataSets("rgba(185, 74, 72, 0.2)", "rgba(185, 74, 72, 1)", "rgba(185, 74, 72, 1)", "#b94a48", null)), + + C(new DataSets("rgba(248, 148, 6,0.2)", "rgba(248, 148, 6,1)", "rgba(248, 148, 6, 1)", "#f89406", null)); + + public static DataSetColor getDataSetColor(String variation) { + if (variation.equalsIgnoreCase("control")) { + return CONTROL; + } else if (variation.equalsIgnoreCase("A")) { + return A; + } else if (variation.equalsIgnoreCase("B")) { + return B; + } else if (variation.equalsIgnoreCase("C")) { + return C; + } else { + return CONTROL; + } + } + + private DataSets m_dataSets; + + private DataSetColor(DataSets dataSets) { + m_dataSets = dataSets; + } + + public DataSets getDataSets() { + return m_dataSets; + } + } + + @SuppressWarnings("unused") + public static class DataSets { + private String m_fillColor; + + private String m_strokeColor; + + private String m_pointColor; + + private String m_pointStrokeColor; + + private List m_data; + + public DataSets() { + } + + public DataSets(String fillColor, String strokeColor, String pointColor, String pointStrokeColor, + List data) { + super(); + m_fillColor = fillColor; + m_strokeColor = strokeColor; + m_pointColor = pointColor; + m_pointStrokeColor = pointStrokeColor; + m_data = data; + } + + public String getPointStrokeColor() { + return m_pointStrokeColor; + } + + public void setData(List data) { + m_data = data; + } + + public String toJson() { + Gson gson = m_gsonBuilderManager.getGsonBuilder().create(); + + return gson.toJson(this, DataSets.class); + } + } + + static class DataSetsBuilder { + public static DataSets buildDataSets(DataSetColor color, List number) { + DataSets dataSets = color.getDataSets(); + dataSets.setData(number); + + return dataSets; + } + } +} diff --git a/cat-home/src/main/java/com/dianping/cat/system/page/abtest/SubHandler.java b/cat-home/src/main/java/com/dianping/cat/system/page/abtest/SubHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..657edfb7342759408ea58bd818e5c5079da844f6 --- /dev/null +++ b/cat-home/src/main/java/com/dianping/cat/system/page/abtest/SubHandler.java @@ -0,0 +1,5 @@ +package com.dianping.cat.system.page.abtest; + +public interface SubHandler { + public void handle(Context ctx, Model model, Payload payload); +} diff --git a/cat-home/src/main/java/com/dianping/cat/system/page/abtest/advisor/ABTestAdvice.java b/cat-home/src/main/java/com/dianping/cat/system/page/abtest/advisor/ABTestAdvice.java new file mode 100644 index 0000000000000000000000000000000000000000..2f435ba7e519a78e3a0cf63b486c2b502beef9f2 --- /dev/null +++ b/cat-home/src/main/java/com/dianping/cat/system/page/abtest/advisor/ABTestAdvice.java @@ -0,0 +1,73 @@ +package com.dianping.cat.system.page.abtest.advisor; + +public class ABTestAdvice { + private double m_ctrOfVariationA = 0.00; + + private double m_ctrOfVariationB = 0.00; + + private double m_difference = 0.00; + + private int m_sizePerGroup = 0; + + private int m_totalParticipants = 0; + + private double m_confidenceInterval = 0.95; + + private int m_days = 0; + + public double getConfidenceInterval() { + return m_confidenceInterval; + } + + public double getCtrOfVariationA() { + return m_ctrOfVariationA; + } + + public double getCtrOfVariationB() { + return m_ctrOfVariationB; + } + + public int getDays() { + return m_days; + } + + public double getDifference() { + return m_difference; + } + + public int getSizePerGroup() { + return m_sizePerGroup; + } + + public int getTotalParticipants() { + return m_totalParticipants; + } + + public void setConfidenceInterval(double confidenceInterval) { + m_confidenceInterval = confidenceInterval; + } + + public void setCtrOfVariationA(double ctrOfVariationA) { + m_ctrOfVariationA = ctrOfVariationA; + } + + public void setCtrOfVariationB(double ctrOfVariationB) { + m_ctrOfVariationB = ctrOfVariationB; + } + + public void setDays(int days) { + m_days = days; + } + + public void setDifference(double difference) { + m_difference = difference; + } + + public void setSizePerGroup(int sizePerGroup) { + m_sizePerGroup = sizePerGroup; + } + + public void setTotalParticipants(int totalParticipants) { + m_totalParticipants = totalParticipants; + } +} \ No newline at end of file diff --git a/cat-home/src/main/java/com/dianping/cat/system/page/abtest/advisor/ABTestAdvisor.java b/cat-home/src/main/java/com/dianping/cat/system/page/abtest/advisor/ABTestAdvisor.java new file mode 100644 index 0000000000000000000000000000000000000000..cdeabe5cb428aca73c00e2e912038887f64594a7 --- /dev/null +++ b/cat-home/src/main/java/com/dianping/cat/system/page/abtest/advisor/ABTestAdvisor.java @@ -0,0 +1,14 @@ +package com.dianping.cat.system.page.abtest.advisor; + +import java.util.List; + +public interface ABTestAdvisor { + + public List offer(double actualCTR, double expectedCTR); + + public void setConfidenceInterval(double interval); + + public void setCurrentPv(int pv); + + public void setDifference(double difference); +} diff --git a/cat-home/src/main/java/com/dianping/cat/system/page/abtest/advisor/ABTestEvaluator.java b/cat-home/src/main/java/com/dianping/cat/system/page/abtest/advisor/ABTestEvaluator.java new file mode 100644 index 0000000000000000000000000000000000000000..f2eda6c646bce8a9dddf2e38ba83fc0461379ed2 --- /dev/null +++ b/cat-home/src/main/java/com/dianping/cat/system/page/abtest/advisor/ABTestEvaluator.java @@ -0,0 +1,54 @@ +package com.dianping.cat.system.page.abtest.advisor; + +public class ABTestEvaluator { + + public float getConfidence(float zscore) { + if (zscore < -3.89) { + return 0; + } else if (zscore > 3.89) { + return 1; + } + + float ret = 0; + float temp = -3.89f; + + while (temp <= zscore) { + ret += 0.0001f * fx(temp); + temp += 0.0001f; + } + + return ret; + } + + public double getConversionRate(double total, double real) { + return real / total; + } + + private float fx(float zscore) { + float ret = 0; + double a = 1.0 / Math.sqrt(Math.PI * 2); + + a = a * Math.pow(Math.E, -0.5 * Math.pow(zscore, 2)); + ret = (float) a; + + return ret; + } + + /* + * 95% confidence interval + */ + public double getSampleSize(double crActual, double crExpected) { + double zscore = 1.65; + double diff = crActual - crExpected; + + return zscore * zscore * (crActual + crExpected - crActual * crActual - crExpected * crExpected) / (diff * diff); + } + + public double getStandardError(double conversionRate, double size) { + return Math.sqrt(conversionRate * (1 - conversionRate) / size); + } + + public double getZsore(double cr1, double cr2, double se1, double se2) { + return Math.abs((cr1 - cr2) / Math.sqrt(se1 * se1 + se2 * se2)); + } +} diff --git a/cat-home/src/main/java/com/dianping/cat/system/page/abtest/advisor/AbstractABTestAdvisor.java b/cat-home/src/main/java/com/dianping/cat/system/page/abtest/advisor/AbstractABTestAdvisor.java new file mode 100644 index 0000000000000000000000000000000000000000..80e0cecc5d2ee6aa2c764eae4afbe79a96d97151 --- /dev/null +++ b/cat-home/src/main/java/com/dianping/cat/system/page/abtest/advisor/AbstractABTestAdvisor.java @@ -0,0 +1,25 @@ +package com.dianping.cat.system.page.abtest.advisor; + +public abstract class AbstractABTestAdvisor implements ABTestAdvisor { + + protected double m_confidenceInterval = 0.95; + + protected int m_pv = 0; + + protected double m_difference = 0.01; + + @Override + public void setConfidenceInterval(double interval) { + m_confidenceInterval = interval; + } + + @Override + public void setCurrentPv(int pv) { + m_pv = pv; + } + + @Override + public void setDifference(double difference) { + m_difference = difference; + } +} diff --git a/cat-home/src/main/java/com/dianping/cat/system/page/abtest/advisor/DefaultABTestAdvisor.java b/cat-home/src/main/java/com/dianping/cat/system/page/abtest/advisor/DefaultABTestAdvisor.java new file mode 100644 index 0000000000000000000000000000000000000000..f0f7a150a02828d5cefd04bdc39858e6a952fef1 --- /dev/null +++ b/cat-home/src/main/java/com/dianping/cat/system/page/abtest/advisor/DefaultABTestAdvisor.java @@ -0,0 +1,53 @@ +package com.dianping.cat.system.page.abtest.advisor; + +import java.util.ArrayList; +import java.util.List; + +public class DefaultABTestAdvisor extends AbstractABTestAdvisor implements ABTestAdvisor { + + @Override + public List offer(double actualCTR, double expectedCTR) { + List advices = new ArrayList(); + + if (actualCTR >= expectedCTR) { + return advices; + } + + double increaseCtr = actualCTR; + do { + ABTestAdvice advice = new ABTestAdvice(); + + advice.setConfidenceInterval(m_confidenceInterval); + advice.setDifference(m_difference); + advice.setCtrOfVariationA(actualCTR); + increaseCtr += m_difference; + advice.setCtrOfVariationB(increaseCtr); + + int sizePerGroup = getSampleSize(actualCTR, increaseCtr); + + advice.setSizePerGroup(sizePerGroup); + advice.setTotalParticipants(sizePerGroup * 2); + + if (m_pv != 0) { + int days = (sizePerGroup * 2) % m_pv == 0 ? (sizePerGroup * 2) / m_pv : (sizePerGroup * 2) / m_pv + 1; + + advice.setDays(days); + } + + advices.add(advice); + } while (increaseCtr < expectedCTR); + + return advices; + } + + private int getSampleSize(double crActual, double crExpected) { + double zscore = 1.65f; + double diff = crActual - crExpected; + + double result = zscore * zscore * (crActual + crExpected - crActual * crActual - crExpected * crExpected) + / (diff * diff); + + return (int) result + 1; + } + +} diff --git a/cat-home/src/main/java/com/dianping/cat/system/page/abtest/conditions/ScriptProvider.java b/cat-home/src/main/java/com/dianping/cat/system/page/abtest/conditions/ScriptProvider.java new file mode 100644 index 0000000000000000000000000000000000000000..e0da7b79205c898424ed922bfb71d62a96cf24db --- /dev/null +++ b/cat-home/src/main/java/com/dianping/cat/system/page/abtest/conditions/ScriptProvider.java @@ -0,0 +1,7 @@ +package com.dianping.cat.system.page.abtest.conditions; + +import java.util.List; + +public interface ScriptProvider { + public List actions(); +} diff --git a/cat-home/src/main/java/com/dianping/cat/system/page/abtest/conditions/URLScriptProvider.java b/cat-home/src/main/java/com/dianping/cat/system/page/abtest/conditions/URLScriptProvider.java new file mode 100644 index 0000000000000000000000000000000000000000..129c137590ee47e275945333ab9b620118d2526d --- /dev/null +++ b/cat-home/src/main/java/com/dianping/cat/system/page/abtest/conditions/URLScriptProvider.java @@ -0,0 +1,96 @@ +package com.dianping.cat.system.page.abtest.conditions; + +import java.util.ArrayList; +import java.util.List; + +import com.dianping.cat.abtest.model.entity.Condition; + +public class URLScriptProvider implements ScriptProvider { + + private String m_actual = "actual"; + + @Override + public List actions() { + List actions = new ArrayList(); + + actions.add("Is equals to (case insens.)"); + actions.add("Is not equals to (case insens.)"); + actions.add("Is equals to (case sens.)"); + actions.add("Is not equals to (case sens.)"); + actions.add("Is equals to (case insens.)"); + actions.add("Marches Regex (case insens.)"); + actions.add("Marches Regex (case sens.)"); + actions.add("contains"); + actions.add("does not contain"); + + return actions; + } + + private String equalsByCaseInsens(String expectedUrl) { + return String.format("%s.equalsIgnoreCase(\"%s\")", m_actual, expectedUrl); + } + + private String notEqualsByCaseInsens(String expectedUrl) { + return String.format("!%s.equalsIgnoreCase(\"%s\")", m_actual, expectedUrl); + } + + private String equalsByCaseSens(String expectedUrl) { + return String.format("%s.equals(\"%s\")", m_actual, expectedUrl); + } + + private String notEqualsByCaseSens(String expectedUrl) { + return String.format("!%s.equals(\"%s\")", m_actual, expectedUrl); + } + + private String marcherByCaseInsens(String expectedUrl) { + int pos = expectedUrl.indexOf('*'); + String subUrl = expectedUrl.substring(0, pos); + + return String.format("%s.toLowerCase().startsWith(\"%s\".toLowerCase())", m_actual, subUrl); + } + + private String marcherByCaseSens(String expectedUrl) { + int pos = expectedUrl.indexOf('*'); + + if(pos > -1){ + String subUrl = expectedUrl.substring(0, pos); + + return String.format("%s.startsWith(\"%s\")", m_actual, subUrl); + }else{ + return "false"; + } + } + + private String contain(String expectedUrl){ + return String.format("%s.indexOf(\"%s\") > -1", m_actual, expectedUrl); + } + + private String notContain(String expectedUrl){ + return String.format("%s.indexOf(\"%s\") == -1", m_actual, expectedUrl); + } + + public String getFragement(Condition condition) { + String expectedUrl = condition.getText(); + + switch (condition.getComparator()) { + case 1: + return equalsByCaseInsens(expectedUrl); + case 2: + return notEqualsByCaseInsens(expectedUrl); + case 3: + return equalsByCaseSens(expectedUrl); + case 4: + return notEqualsByCaseSens(expectedUrl); + case 5: + return marcherByCaseInsens(expectedUrl); + case 6: + return marcherByCaseSens(expectedUrl); + case 7: + return contain(expectedUrl); + case 8: + return notContain(expectedUrl); + } + + return "false"; + } +} diff --git a/cat-home/src/main/java/com/dianping/cat/system/page/abtest/service/ABTestService.java b/cat-home/src/main/java/com/dianping/cat/system/page/abtest/service/ABTestService.java index 16eca8229b4be26e02f5dce86c2662440bec9e43..16045eca275d1351bcd60d9e8a31cef2ff80b74a 100644 --- a/cat-home/src/main/java/com/dianping/cat/system/page/abtest/service/ABTestService.java +++ b/cat-home/src/main/java/com/dianping/cat/system/page/abtest/service/ABTestService.java @@ -1,8 +1,36 @@ package com.dianping.cat.system.page.abtest.service; +import java.util.List; +import java.util.Map; + +import com.dianping.cat.abtest.model.entity.AbtestModel; +import com.dianping.cat.core.dal.Project; import com.dianping.cat.home.dal.abtest.Abtest; +import com.dianping.cat.home.dal.abtest.AbtestRun; +import com.dianping.cat.home.dal.abtest.GroupStrategy; +import com.dianping.cat.system.page.abtest.AbtestStatus; public interface ABTestService { - public Abtest getABTestNameByRunId(int id); + public Abtest getABTestByRunId(int id); + + public AbtestModel getAbtestModelByStatus(AbtestStatus... status); + + public AbtestModel getAbtestModelByRunID(int runId); + + public GroupStrategy getGroupStrategyById(int id); + + public AbtestRun getAbtestRunById(int id); + + public List getAbtestRunByStatus(AbtestStatus status); + + public List getAllGroupStrategies(); + + public Map> getAllProjects(); + + public void refresh(); + + public void setModified(); + + public long getModifiedTime(); } diff --git a/cat-home/src/main/java/com/dianping/cat/system/page/abtest/service/ABTestServiceImpl.java b/cat-home/src/main/java/com/dianping/cat/system/page/abtest/service/ABTestServiceImpl.java index 7dd6f94aa3bb147803a16c56f99e7636cefdeb41..144b067c1c2b4b2e35f90100a3f0980f5c7cd68d 100644 --- a/cat-home/src/main/java/com/dianping/cat/system/page/abtest/service/ABTestServiceImpl.java +++ b/cat-home/src/main/java/com/dianping/cat/system/page/abtest/service/ABTestServiceImpl.java @@ -1,20 +1,46 @@ package com.dianping.cat.system.page.abtest.service; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; import java.util.Map; +import java.util.TreeMap; +import java.util.concurrent.locks.LockSupport; +import org.apache.commons.lang.StringUtils; +import org.codehaus.plexus.personality.plexus.lifecycle.phase.Initializable; +import org.codehaus.plexus.personality.plexus.lifecycle.phase.InitializationException; import org.jboss.netty.util.internal.ConcurrentHashMap; import org.unidal.dal.jdbc.DalException; +import org.unidal.helper.Threads.Task; import org.unidal.lookup.annotation.Inject; import com.dianping.cat.Cat; +import com.dianping.cat.abtest.model.entity.AbtestModel; +import com.dianping.cat.abtest.model.entity.Case; +import com.dianping.cat.abtest.model.entity.Condition; +import com.dianping.cat.abtest.model.entity.ConversionRule; +import com.dianping.cat.abtest.model.entity.GroupstrategyDescriptor; +import com.dianping.cat.abtest.model.entity.Run; +import com.dianping.cat.core.dal.Project; +import com.dianping.cat.core.dal.ProjectDao; +import com.dianping.cat.core.dal.ProjectEntity; import com.dianping.cat.home.dal.abtest.Abtest; import com.dianping.cat.home.dal.abtest.AbtestDao; import com.dianping.cat.home.dal.abtest.AbtestEntity; import com.dianping.cat.home.dal.abtest.AbtestRun; import com.dianping.cat.home.dal.abtest.AbtestRunDao; import com.dianping.cat.home.dal.abtest.AbtestRunEntity; +import com.dianping.cat.home.dal.abtest.GroupStrategy; +import com.dianping.cat.home.dal.abtest.GroupStrategyDao; +import com.dianping.cat.home.dal.abtest.GroupStrategyEntity; +import com.dianping.cat.system.page.abtest.AbtestStatus; +import com.dianping.cat.system.page.abtest.GsonBuilderManager; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; -public class ABTestServiceImpl implements ABTestService { +public class ABTestServiceImpl implements ABTestService, Initializable, Task { @Inject private AbtestDao m_abtestDao; @@ -22,25 +48,351 @@ public class ABTestServiceImpl implements ABTestService { @Inject private AbtestRunDao m_abtestRunDao; + @Inject + private GroupStrategyDao m_groupStrategyDao; + + @Inject + private GsonBuilderManager m_gsonBuilderManager; + + @Inject + private ProjectDao m_projectDao; + + @Inject + private int m_refreshTimeInSeconds = 60; + private Map m_abtestMap = new ConcurrentHashMap(); + private Map m_abtestRunMap = new ConcurrentHashMap(); + + private Map m_groupStrategyMap = new ConcurrentHashMap(); + + private long m_lastRefreshTime = -1; + + private long m_modifyTime = 0; + + private Type m_listType = new TypeToken>() { + }.getType(); + + private Type m_ruleType = new TypeToken>() { + }.getType(); + @Override - public Abtest getABTestNameByRunId(int id) { - Abtest name = m_abtestMap.get(id); + public Abtest getABTestByRunId(int id) { + AbtestRun run = getAbtestRunById(id); + Abtest ab = null; + + if (run != null) { + ab = m_abtestMap.get(run.getCaseId()); + + if (ab == null) { + try { + ab = m_abtestDao.findByPK(run.getCaseId(), AbtestEntity.READSET_FULL); + + m_abtestMap.put(run.getCaseId(), ab); + } catch (Throwable e) { + Cat.logError(e); + } + } + } + + return ab; + } + + public AbtestModel getAbtestModelByRunID(int runId) { + AbtestModel model = new AbtestModel(); + + Abtest abtest = getABTestByRunId(runId); + AbtestRun run = getAbtestRunById(runId); + + if (abtest != null && run != null) { + GroupStrategy groupStrategy = getGroupStrategyById(abtest.getGroupStrategy()); + Case _case = transform(abtest, run, groupStrategy); + + model.addCase(_case); + } + + return model; + } + + @Override + public AbtestModel getAbtestModelByStatus(AbtestStatus... status) { + AbtestModel model = new AbtestModel(); + + if (!m_abtestRunMap.isEmpty()) { + Date now = new Date(); + + for (AbtestRun run : m_abtestRunMap.values()) { + Abtest entity = getABTestByRunId(run.getId()); + + if (entity != null) { + GroupStrategy groupStrategy = getGroupStrategyById(entity.getGroupStrategy()); + + if (groupStrategy != null) { + if (status.length == 0) { + Case _case = transform(entity, run, groupStrategy); + model.addCase(_case); + } else { + AbtestStatus _status = AbtestStatus.calculateStatus(run, now); + + for (AbtestStatus st : status) { + if (st == _status) { + Case _case = transform(entity, run, groupStrategy); + model.addCase(_case); + } + } + } + } + } + } + } - if (name != null) { - return name; - } else { + return model; + } + + @Override + public AbtestRun getAbtestRunById(int id) { + AbtestRun abtetRun = m_abtestRunMap.get(id); + + if (abtetRun == null) { try { - AbtestRun run = m_abtestRunDao.findByPK(id, AbtestRunEntity.READSET_FULL); - Abtest abtest = m_abtestDao.findByPK(run.getCaseId(), AbtestEntity.READSET_FULL); + abtetRun = m_abtestRunDao.findByPK(id, AbtestRunEntity.READSET_FULL); - m_abtestMap.put(id, abtest); - } catch (DalException e) { + m_abtestRunMap.put(id, abtetRun); + } catch (Throwable e) { Cat.logError(e); } } + return abtetRun; + } + + @Override + public List getAllGroupStrategies() { + try { + return m_groupStrategyDao.findAllByStatus(1, GroupStrategyEntity.READSET_FULL); + } catch (DalException e) { + Cat.logError(e); + } + return null; } + + @Override + public Map> getAllProjects() { + List projects = new ArrayList(); + + try { + projects = m_projectDao.findAll(ProjectEntity.READSET_FULL); + } catch (Exception e) { + Cat.logError(e); + } + + Map> result = new TreeMap>(); + if (projects != null) { + for (Project project : projects) { + String key = project.getDepartment() + "-" + project.getProjectLine(); + List list = result.get(key); + if (list == null) { + list = new ArrayList(); + result.put(key, list); + } + list.add(project); + } + } + + return result; + } + + @Override + public GroupStrategy getGroupStrategyById(int id) { + GroupStrategy groupStrategy = m_groupStrategyMap.get(id); + + if (groupStrategy == null) { + try { + groupStrategy = m_groupStrategyDao.findByPK(id, GroupStrategyEntity.READSET_FULL); + + m_groupStrategyMap.put(id, groupStrategy); + } catch (Throwable e) { + Cat.logError(e); + } + } + + return groupStrategy; + } + + @Override + public long getModifiedTime() { + return m_modifyTime; + } + + @Override + public String getName() { + return getClass().getSimpleName(); + } + + @Override + public void initialize() throws InitializationException { + refresh(); + } + + @Override + public void refresh() { + if (m_modifyTime > m_lastRefreshTime) { + try { + Map abtestMap = new ConcurrentHashMap(); + Map abtestRunMap = new ConcurrentHashMap(); + Map groupStrategyMap = new ConcurrentHashMap(); + + List abtests = m_abtestDao.findAll(AbtestEntity.READSET_FULL); + + for (Abtest abtest : abtests) { + abtestMap.put(abtest.getId(), abtest); + } + + List abtestRuns = m_abtestRunDao.findAll(AbtestRunEntity.READSET_FULL); + + for (AbtestRun abtestRun : abtestRuns) { + abtestRunMap.put(abtestRun.getId(), abtestRun); + } + + List groupStrategies = m_groupStrategyDao.findAll(GroupStrategyEntity.READSET_FULL); + + for (GroupStrategy groupStrategy : groupStrategies) { + groupStrategyMap.put(groupStrategy.getId(), groupStrategy); + } + + // switch + m_abtestMap = abtestMap; + m_abtestRunMap = abtestRunMap; + m_groupStrategyMap = groupStrategyMap; + + m_lastRefreshTime = m_modifyTime; + } catch (Throwable e) { + Cat.logError(e); + } + } + } + + @Override + public void run() { + while (true) { + long start = System.currentTimeMillis(); + + try { + refresh(); + } catch (Throwable e) { + Cat.logError(e); + } + + LockSupport.parkUntil(start + m_refreshTimeInSeconds * 1000L); // every minute + } + } + + public void setAbtestDao(AbtestDao abtestDao) { + m_abtestDao = abtestDao; + } + + public void setAbtestMap(Map abtestMap) { + m_abtestMap = abtestMap; + } + + public void setAbtestRunDao(AbtestRunDao abtestRunDao) { + m_abtestRunDao = abtestRunDao; + } + + public void setAbtestRunMap(Map abtestRunMap) { + m_abtestRunMap = abtestRunMap; + } + + public void setGroupStrategyDao(GroupStrategyDao groupStrategyDao) { + m_groupStrategyDao = groupStrategyDao; + } + + public void setGroupStrategyMap(Map groupStrategyMap) { + m_groupStrategyMap = groupStrategyMap; + } + + public void setGsonBuilderManager(GsonBuilderManager gsonBuilderManager) { + m_gsonBuilderManager = gsonBuilderManager; + } + + @Override + public synchronized void setModified() { + m_modifyTime = System.currentTimeMillis(); + } + + public void setProjectDao(ProjectDao projectDao) { + m_projectDao = projectDao; + } + + public void setRefreshTimeInSeconds(int refreshTimeInSeconds) { + m_refreshTimeInSeconds = refreshTimeInSeconds; + } + + @Override + public void shutdown() { + } + + private Case transform(Abtest abtest, AbtestRun run, GroupStrategy groupStrategy) { + Case abCase = new Case(abtest.getId()); + + abCase.setCreatedDate(abtest.getCreationDate()); + abCase.setDescription(abtest.getDescription()); + abCase.setGroupStrategy(groupStrategy.getName()); + abCase.setName(abtest.getName()); + abCase.setOwner(abtest.getOwner()); + abCase.setLastModifiedDate(abtest.getModifiedDate()); + for (String domain : StringUtils.split(abtest.getDomains(), ',')) { + abCase.addDomain(domain); + } + + Run abRun = new Run(run.getId()); + Gson gson = m_gsonBuilderManager.getGsonBuilder().create(); + + for (String domain : StringUtils.split(run.getDomains(), ',')) { + abRun.addDomain(domain); + } + abRun.setCreator(run.getCreator()); + abRun.setConditionsFragement(run.getJavaFragement()); + abRun.setDisabled(false); + abRun.setEndDate(run.getEndDate()); + abRun.setStartDate(run.getStartDate()); + abRun.setCreatedDate(run.getCreationDate()); + abRun.setLastModifiedDate(run.getModifiedDate()); + + if (StringUtils.isNotBlank(run.getStrategyConfiguration())) { + abRun.setGroupstrategyDescriptor(gson.fromJson(run.getStrategyConfiguration(), GroupstrategyDescriptor.class)); + } + + if (StringUtils.isNotBlank(run.getConditions())) { + List conditions = gson.fromJson(run.getConditions(), m_listType); + + abRun.getConditions().addAll(conditions); + } + + if (StringUtils.isNotBlank(run.getConversionGoals())) { + List conversions = gson.fromJson(run.getConversionGoals(), m_ruleType); + + abRun.getConversionRules().addAll(conversions); + } + + abCase.addRun(abRun); + return abCase; + } + + @Override + public List getAbtestRunByStatus(AbtestStatus status) { + List runs = new ArrayList(); + Date now = new Date(); + + for (AbtestRun run : m_abtestRunMap.values()) { + AbtestStatus _status = AbtestStatus.calculateStatus(run, now); + + if (_status == status) { + runs.add(run); + } + } + + return runs; + } } diff --git a/cat-home/src/main/resources/META-INF/dal/jdbc/abtest-codegen.xml b/cat-home/src/main/resources/META-INF/dal/jdbc/abtest-codegen.xml index a87366aae8323b10a45ba6bb8184c46e4bc4b48c..41b85deed7798e4bca6c96ae354ce97989e383d1 100644 --- a/cat-home/src/main/resources/META-INF/dal/jdbc/abtest-codegen.xml +++ b/cat-home/src/main/resources/META-INF/dal/jdbc/abtest-codegen.xml @@ -42,7 +42,45 @@ - + + + + + + + + + + + + + + + + + + + FROM + WHERE = ${key-id}]]> + + + () + VALUES()]]> + + + + + SET + WHERE = ${key-id}]]> + + + + + WHERE = ${key-id}]]> + + + + @@ -50,6 +88,9 @@ + + + @@ -88,9 +129,9 @@ - - - + + + diff --git a/cat-home/src/main/resources/META-INF/dal/jdbc/abtest-dal.xml b/cat-home/src/main/resources/META-INF/dal/jdbc/abtest-dal.xml index 13f356dddb358ca62c8a93c34691dac1a678921c..dee24ff5f867d02da36688fb735b4a8cb36e4366 100644 --- a/cat-home/src/main/resources/META-INF/dal/jdbc/abtest-dal.xml +++ b/cat-home/src/main/resources/META-INF/dal/jdbc/abtest-dal.xml @@ -26,7 +26,10 @@ - + + + + @@ -58,7 +61,51 @@ WHERE = ${status} ]]> + + + + FROM
+ WHERE = ${name} + ]]> + + + + FROM
+ ]]> + + + + + + + + + + + + FROM
+ WHERE = ${run-id} + AND >= ${start-date} + AND <= ${end-date} + ]]> + + + + + + + FROM
+ WHERE = ${run-id} + ORDER BY DESC LIMIT 1 + ]]> + + + diff --git a/cat-home/src/main/resources/META-INF/dal/jdbc/report-codegen.xml b/cat-home/src/main/resources/META-INF/dal/jdbc/report-codegen.xml index 8d5b33dc134ef05f457a13791b6750fd05cd7496..e7411ae33075b9bdb00f11ae31e986d3dd1ca544 100644 --- a/cat-home/src/main/resources/META-INF/dal/jdbc/report-codegen.xml +++ b/cat-home/src/main/resources/META-INF/dal/jdbc/report-codegen.xml @@ -51,6 +51,7 @@ + diff --git a/cat-home/src/main/resources/META-INF/dal/model/abtest-report-codegen.xml b/cat-home/src/main/resources/META-INF/dal/model/abtest-report-codegen.xml new file mode 100644 index 0000000000000000000000000000000000000000..8170b043c19b1c4fd256122b301aed9e931f15c7 --- /dev/null +++ b/cat-home/src/main/resources/META-INF/dal/model/abtest-report-codegen.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cat-home/src/main/resources/META-INF/dal/model/abtest-report-manifest.xml b/cat-home/src/main/resources/META-INF/dal/model/abtest-report-manifest.xml new file mode 100644 index 0000000000000000000000000000000000000000..e703b09764b3f4ca4f60c6886b8ec0cbb392c644 --- /dev/null +++ b/cat-home/src/main/resources/META-INF/dal/model/abtest-report-manifest.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/cat-home/src/main/resources/META-INF/dal/model/abtest-report-model.xml b/cat-home/src/main/resources/META-INF/dal/model/abtest-report-model.xml new file mode 100644 index 0000000000000000000000000000000000000000..e3cf3bf409feadf54227c13cbddb26319ac81b3f --- /dev/null +++ b/cat-home/src/main/resources/META-INF/dal/model/abtest-report-model.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cat-home/src/main/resources/META-INF/plexus/components.xml b/cat-home/src/main/resources/META-INF/plexus/components.xml index 5fa8c308bd77fafb1329da5cb8f3e2c707c64ed6..63989f6111478b3a6e2c046b4904af247fb7203a 100755 --- a/cat-home/src/main/resources/META-INF/plexus/components.xml +++ b/cat-home/src/main/resources/META-INF/plexus/components.xml @@ -762,6 +762,24 @@ + + com.dianping.cat.report.task.abtest.ABTestReportBuilder + com.dianping.cat.report.task.abtest.ABTestReportBuilder + + + com.dianping.cat.report.service.ReportService + + + com.dianping.cat.home.dal.abtest.AbtestReportDao + + + com.dianping.cat.consumer.advanced.ProductLineConfigManager + + + com.dianping.cat.system.page.abtest.service.ABTestService + + + com.dianping.cat.report.task.spi.ReportFacade com.dianping.cat.report.task.spi.ReportFacade @@ -808,6 +826,9 @@ com.dianping.cat.report.task.utilization.UtilizationReportBuilder + + com.dianping.cat.report.task.abtest.ABTestReportBuilder + @@ -1370,7 +1391,7 @@ com.mysql.jdbc.Driver jdbc:mysql://127.0.0.1:3306/cat root - password + @@ -1504,6 +1525,16 @@ cat + + org.unidal.dal.jdbc.mapping.TableProvider + abtest-report + org.unidal.dal.jdbc.mapping.SimpleTableProvider + + abtest-report + abtest_report + cat + + org.unidal.dal.jdbc.mapping.TableProvider abtest-run @@ -1533,6 +1564,15 @@ + + com.dianping.cat.home.dal.abtest.AbtestReportDao + com.dianping.cat.home.dal.abtest.AbtestReportDao + + + org.unidal.dal.jdbc.QueryEngine + + + com.dianping.cat.home.dal.abtest.AbtestRunDao com.dianping.cat.home.dal.abtest.AbtestRunDao @@ -1594,9 +1634,57 @@ + + com.dianping.cat.system.page.abtest.GroupStrategyParser + com.dianping.cat.system.page.abtest.GroupStrategyParser + + + com.dianping.cat.system.page.abtest.GsonBuilderManager + com.dianping.cat.system.page.abtest.GsonBuilderManager + + + com.dianping.cat.system.page.abtest.advisor.ABTestAdvisor + com.dianping.cat.system.page.abtest.advisor.DefaultABTestAdvisor + + + com.dianping.cat.system.page.abtest.ListViewHandler + com.dianping.cat.system.page.abtest.ListViewHandler + + 10 + + + + com.dianping.cat.home.dal.abtest.AbtestDao + + + com.dianping.cat.home.dal.abtest.AbtestRunDao + + + + + com.dianping.cat.system.page.abtest.ReportHandler + com.dianping.cat.system.page.abtest.ReportHandler + + + com.dianping.cat.home.dal.abtest.AbtestDao + + + com.dianping.cat.home.dal.abtest.AbtestRunDao + + + com.dianping.cat.home.dal.abtest.AbtestReportDao + + + com.dianping.cat.consumer.advanced.MetricConfigManager + + + com.dianping.cat.system.page.abtest.service.ABTestService com.dianping.cat.system.page.abtest.service.ABTestServiceImpl + + 60 + com.dianping.cat.home.dal.abtest.AbtestDao @@ -1604,6 +1692,15 @@ com.dianping.cat.home.dal.abtest.AbtestRunDao + + com.dianping.cat.home.dal.abtest.GroupStrategyDao + + + com.dianping.cat.core.dal.ProjectDao + + + com.dianping.cat.system.page.abtest.GsonBuilderManager + @@ -2966,10 +3063,25 @@ com.dianping.cat.home.dal.abtest.GroupStrategyDao - com.dianping.cat.core.dal.ProjectDao + com.dianping.cat.system.page.abtest.JspViewer - com.dianping.cat.system.page.abtest.JspViewer + com.dianping.cat.system.page.abtest.GroupStrategyParser + + + com.dianping.cat.system.page.abtest.service.ABTestService + + + com.dianping.cat.system.page.abtest.advisor.ABTestAdvisor + + + com.dianping.cat.system.page.abtest.ListViewHandler + + + com.dianping.cat.system.page.abtest.ReportHandler + + + com.dianping.cat.system.page.abtest.GsonBuilderManager @@ -3009,6 +3121,56 @@ + + com.dianping.cat.system.page.abtest.GroupStrategyParser + com.dianping.cat.system.page.abtest.GroupStrategyParser + + + com.dianping.cat.system.page.abtest.ListViewHandler + com.dianping.cat.system.page.abtest.ListViewHandler + + + com.dianping.cat.home.dal.abtest.AbtestDao + + + com.dianping.cat.home.dal.abtest.AbtestRunDao + + + int + + + + + com.dianping.cat.system.page.abtest.ReportHandler + com.dianping.cat.system.page.abtest.ReportHandler + + + com.dianping.cat.home.dal.abtest.AbtestDao + + + com.dianping.cat.home.dal.abtest.AbtestRunDao + + + com.dianping.cat.home.dal.abtest.AbtestReportDao + + + com.dianping.cat.consumer.advanced.MetricConfigManager + + + + + com.dianping.cat.home.dal.abtest.AbtestReportDao + com.dianping.cat.home.dal.abtest.AbtestReportDao + + + org.unidal.dal.jdbc.QueryEngine + + + + + com.dianping.cat.system.page.abtest.GsonBuilderManager + com.dianping.cat.system.page.abtest.GsonBuilderManager + com.dianping.cat.system.page.plugin.Handler com.dianping.cat.system.page.plugin.Handler diff --git a/cat-home/src/main/resources/META-INF/wizard/jdbc/wizard.xml b/cat-home/src/main/resources/META-INF/wizard/jdbc/wizard.xml index c2b9dacb34eea7c117e4d2c42acffb18386b2bbd..f3126751fb228cd345f578c7bb3e87271beee1a4 100644 --- a/cat-home/src/main/resources/META-INF/wizard/jdbc/wizard.xml +++ b/cat-home/src/main/resources/META-INF/wizard/jdbc/wizard.xml @@ -5,7 +5,7 @@ com.mysql.jdbc.Driver jdbc:mysql://127.0.0.1:3306/cat root - password + useUnicode=true&autoReconnect=true @@ -22,6 +22,7 @@
+
diff --git a/cat-home/src/main/resources/META-INF/wizard/model/wizard.xml b/cat-home/src/main/resources/META-INF/wizard/model/wizard.xml index 81a64f0d8a9aac686c5a06119dcdbc2cdb09b737..e5704886315a7b5c3bebd2afc80c503796803595 100644 --- a/cat-home/src/main/resources/META-INF/wizard/model/wizard.xml +++ b/cat-home/src/main/resources/META-INF/wizard/model/wizard.xml @@ -9,6 +9,9 @@ src/test/resources/com/dianping/cat/system/config/info.xml + + src/test/resources/com/dianping/cat/report/page/abtest/abtest-report.xml + src/test/resources/com/dianping/cat/report/page/dependency/exception-threshold-config.xml diff --git a/cat-home/src/main/resources/freemaker/scriptFragement.ftl b/cat-home/src/main/resources/freemaker/scriptFragement.ftl new file mode 100644 index 0000000000000000000000000000000000000000..51182b191373939ea94da792f83463ca85539b45 --- /dev/null +++ b/cat-home/src/main/resources/freemaker/scriptFragement.ftl @@ -0,0 +1,105 @@ +package com.dianping.cat.abtest.conditions; + +import java.util.Random; +import javax.servlet.http.HttpServletRequest; +import com.dianping.cat.abtest.spi.internal.conditions.ABTestCondition; + +public class TrafficFilter { + +<#assign count = 1> +<#list run.conditions as condition> + private Condition${count} m_condition${count} = new Condition${count}(); + + <#assign count = count + 1> + + + <#assign count1 = 1> + <#assign isFirst1 = 1> + <#assign isFirst2 = 1> + public boolean isEligible(HttpServletRequest request) { + if( + <#list run.conditions as condition> + <#if isFirst1 = 0> + <#if condition.seq = 4> + <#if isFirst2 = 0> + ) + + + <#if operator = 0> + && + + <#if operator = 1> + || + + <#if condition.seq = 3> + <#if isFirst2 = 1> + ( + <#assign isFirst2 = 0> + + + + m_condition${count1}.accept(request) + <#if condition.operator = "and"> + <#assign operator = 0> + + <#if condition.operator = "or"> + <#assign operator = 1> + + + <#assign isFirst1 = 0> + <#assign count1 = count1 + 1> + + ) { + return true; + } + + return false; + } + + <#assign count1 = 1> + <#list run.conditions as condition> + <#if condition.name = "url"> + public class Condition${count1} implements ABTestCondition { + @Override + public boolean accept(HttpServletRequest request) { + String actual = request.getRequestURL().toString(); + + if (${urlScriptProvider.getFragement(condition)}) { + return true; + } else { + return false; + } + } + } + + + <#if condition.name = "percent"> + public class Condition${count1} implements ABTestCondition { + private int m_percent = -1; + + private Random m_random = new Random(); + + @Override + public boolean accept(HttpServletRequest request) { + if (m_percent == -1) { + m_percent = ${condition.text}; + } + + if (m_percent == 100) { + return true; + } + + int random = m_random.nextInt(100) + 1; + + if (random <= m_percent) { + return true; + } else { + return false; + } + } + } + + + <#assign count1 = count1 + 1> + +} \ No newline at end of file diff --git a/cat-home/src/main/webapp/css/body.css b/cat-home/src/main/webapp/css/body.css index 443ed8940a980ba42ebea7cff090214c5b8c8ad0..915c8bc3ae9417e85b4ca0c26646a4f0bdca3476 100755 --- a/cat-home/src/main/webapp/css/body.css +++ b/cat-home/src/main/webapp/css/body.css @@ -248,12 +248,6 @@ a.abtest-heartbeat { margin-left: 0px; } -.form-horizontal .control-label{ - width:50px; -} -.form-horizontal .controls{ - margin-left:70px; -} select{ margin-bottom:0px; } diff --git a/cat-home/src/main/webapp/css/rickshaw.css b/cat-home/src/main/webapp/css/rickshaw.css new file mode 100644 index 0000000000000000000000000000000000000000..3b7e4a94ab59ee4683d31abb1eae45f41a88dfd1 --- /dev/null +++ b/cat-home/src/main/webapp/css/rickshaw.css @@ -0,0 +1,320 @@ +.rickshaw_graph .detail { + pointer-events: none; + position: absolute; + top: 0; + z-index: 2; + background: rgba(0, 0, 0, 0.1); + bottom: 0; + width: 1px; + transition: opacity 0.25s linear; + -moz-transition: opacity 0.25s linear; + -o-transition: opacity 0.25s linear; + -webkit-transition: opacity 0.25s linear; +} +.rickshaw_graph .detail.inactive { + opacity: 0; +} +.rickshaw_graph .detail .item.active { + opacity: 1; +} +.rickshaw_graph .detail .x_label { + font-family: Arial, sans-serif; + border-radius: 3px; + padding: 6px; + opacity: 0.5; + border: 1px solid #e0e0e0; + font-size: 12px; + position: absolute; + background: white; + white-space: nowrap; +} +.rickshaw_graph .detail .item { + position: absolute; + z-index: 2; + border-radius: 3px; + padding: 0.25em; + font-size: 12px; + font-family: Arial, sans-serif; + opacity: 0; + background: rgba(0, 0, 0, 0.4); + color: white; + border: 1px solid rgba(0, 0, 0, 0.4); + margin-left: 1em; + margin-top: -1em; + white-space: nowrap; +} +.rickshaw_graph .detail .item.active { + opacity: 1; + background: rgba(0, 0, 0, 0.8); +} +.rickshaw_graph .detail .item:before { + content: "\25c2"; + position: absolute; + left: -0.5em; + color: rgba(0, 0, 0, 0.7); + width: 0; +} +.rickshaw_graph .detail .dot { + width: 4px; + height: 4px; + margin-left: -4px; + margin-top: -3px; + border-radius: 5px; + position: absolute; + box-shadow: 0 0 2px rgba(0, 0, 0, 0.6); + background: white; + border-width: 2px; + border-style: solid; + display: none; + background-clip: padding-box; +} +.rickshaw_graph .detail .dot.active { + display: block; +} +/* graph */ + +.rickshaw_graph { + position: relative; +} +.rickshaw_graph svg { + display: block; + overflow: hidden; +} + +/* ticks */ + +.rickshaw_graph .x_tick { + position: absolute; + top: 0; + bottom: 0; + width: 0px; + border-left: 1px dotted rgba(0, 0, 0, 0.2); + pointer-events: none; +} +.rickshaw_graph .x_tick .title { + position: absolute; + font-size: 12px; + font-family: Arial, sans-serif; + opacity: 0.5; + white-space: nowrap; + margin-left: 3px; + bottom: 1px; +} + +/* annotations */ + +.rickshaw_annotation_timeline { + height: 1px; + border-top: 1px solid #e0e0e0; + margin-top: 10px; + position: relative; +} +.rickshaw_annotation_timeline .annotation { + position: absolute; + height: 6px; + width: 6px; + margin-left: -2px; + top: -3px; + border-radius: 5px; + background-color: rgba(0, 0, 0, 0.25); +} +.rickshaw_graph .annotation_line { + position: absolute; + top: 0; + bottom: -6px; + width: 0px; + border-left: 2px solid rgba(0, 0, 0, 0.3); + display: none; +} +.rickshaw_graph .annotation_line.active { + display: block; +} + +.rickshaw_graph .annotation_range { + background: rgba(0, 0, 0, 0.1); + display: none; + position: absolute; + top: 0; + bottom: -6px; +} +.rickshaw_graph .annotation_range.active { + display: block; +} +.rickshaw_graph .annotation_range.active.offscreen { + display: none; +} + +.rickshaw_annotation_timeline .annotation .content { + background: white; + color: black; + opacity: 0.9; + padding: 5px 5px; + box-shadow: 0 0 2px rgba(0, 0, 0, 0.8); + border-radius: 3px; + position: relative; + z-index: 20; + font-size: 12px; + padding: 6px 8px 8px; + top: 18px; + left: -11px; + width: 160px; + display: none; + cursor: pointer; +} +.rickshaw_annotation_timeline .annotation .content:before { + content: "\25b2"; + position: absolute; + top: -11px; + color: white; + text-shadow: 0 -1px 1px rgba(0, 0, 0, 0.8); +} +.rickshaw_annotation_timeline .annotation.active, +.rickshaw_annotation_timeline .annotation:hover { + background-color: rgba(0, 0, 0, 0.8); + cursor: none; +} +.rickshaw_annotation_timeline .annotation .content:hover { + z-index: 50; +} +.rickshaw_annotation_timeline .annotation.active .content { + display: block; +} +.rickshaw_annotation_timeline .annotation:hover .content { + display: block; + z-index: 50; +} +.rickshaw_graph .y_axis, +.rickshaw_graph .x_axis_d3 { + fill: none; +} +.rickshaw_graph .y_ticks .tick, +.rickshaw_graph .x_ticks_d3 .tick { + stroke: rgba(0, 0, 0, 0.16); + stroke-width: 2px; + shape-rendering: crisp-edges; + pointer-events: none; +} +.rickshaw_graph .y_grid .tick, +.rickshaw_graph .x_grid_d3 .tick { + z-index: -1; + stroke: rgba(0, 0, 0, 0.20); + stroke-width: 1px; + stroke-dasharray: 1 1; +} +.rickshaw_graph .y_grid path, +.rickshaw_graph .x_grid_d3 path { + fill: none; + stroke: none; +} +.rickshaw_graph .y_ticks path, +.rickshaw_graph .x_ticks_d3 path { + fill: none; + stroke: #808080; +} +.rickshaw_graph .y_ticks text, +.rickshaw_graph .x_ticks_d3 text { + opacity: 0.5; + font-size: 12px; + pointer-events: none; +} +.rickshaw_graph .x_tick.glow .title, +.rickshaw_graph .y_ticks.glow text { + fill: black; + color: black; + text-shadow: + -1px 1px 0 rgba(255, 255, 255, 0.1), + 1px -1px 0 rgba(255, 255, 255, 0.1), + 1px 1px 0 rgba(255, 255, 255, 0.1), + 0px 1px 0 rgba(255, 255, 255, 0.1), + 0px -1px 0 rgba(255, 255, 255, 0.1), + 1px 0px 0 rgba(255, 255, 255, 0.1), + -1px 0px 0 rgba(255, 255, 255, 0.1), + -1px -1px 0 rgba(255, 255, 255, 0.1); +} +.rickshaw_graph .x_tick.inverse .title, +.rickshaw_graph .y_ticks.inverse text { + fill: white; + color: white; + text-shadow: + -1px 1px 0 rgba(0, 0, 0, 0.8), + 1px -1px 0 rgba(0, 0, 0, 0.8), + 1px 1px 0 rgba(0, 0, 0, 0.8), + 0px 1px 0 rgba(0, 0, 0, 0.8), + 0px -1px 0 rgba(0, 0, 0, 0.8), + 1px 0px 0 rgba(0, 0, 0, 0.8), + -1px 0px 0 rgba(0, 0, 0, 0.8), + -1px -1px 0 rgba(0, 0, 0, 0.8); +} +.rickshaw_legend { + font-family: Arial; + font-size: 12px; + color: white; + background: #404040; + display: inline-block; + padding: 12px 5px; + border-radius: 2px; + position: relative; +} +.rickshaw_legend:hover { + z-index: 10; +} +.rickshaw_legend .swatch { + width: 10px; + height: 10px; + border: 1px solid rgba(0, 0, 0, 0.2); +} +.rickshaw_legend .line { + clear: both; + line-height: 140%; + padding-right: 15px; +} +.rickshaw_legend .line .swatch { + display: inline-block; + margin-right: 3px; + border-radius: 2px; +} +.rickshaw_legend .label { + margin: 0; + white-space: nowrap; + display: inline; + font-size: inherit; + background-color: transparent; + color: inherit; + font-weight: normal; + line-height: normal; + padding: 0px; + text-shadow: none; +} +.rickshaw_legend .action:hover { + opacity: 0.6; +} +.rickshaw_legend .action { + margin-right: 0.2em; + font-size: 10px; + opacity: 0.2; + cursor: pointer; + font-size: 14px; +} +.rickshaw_legend .line.disabled { + opacity: 0.4; +} +.rickshaw_legend ul { + list-style-type: none; + margin: 0; + padding: 0; + margin: 2px; + cursor: pointer; +} +.rickshaw_legend li { + padding: 0 0 0 2px; + min-width: 80px; + white-space: nowrap; +} +.rickshaw_legend li:hover { + background: rgba(255, 255, 255, 0.08); + border-radius: 3px; +} +.rickshaw_legend li:active { + background: rgba(255, 255, 255, 0.2); + border-radius: 3px; +} diff --git a/cat-home/src/main/webapp/css/rickshaw.min.css b/cat-home/src/main/webapp/css/rickshaw.min.css new file mode 100644 index 0000000000000000000000000000000000000000..8f8414d57201e3fd53b604e6c6b07e04fd636776 --- /dev/null +++ b/cat-home/src/main/webapp/css/rickshaw.min.css @@ -0,0 +1 @@ +.rickshaw_graph .detail{pointer-events:none;position:absolute;top:0;z-index:2;background:rgba(0,0,0,.1);bottom:0;width:1px;transition:opacity .25s linear;-moz-transition:opacity .25s linear;-o-transition:opacity .25s linear;-webkit-transition:opacity .25s linear}.rickshaw_graph .detail.inactive{opacity:0}.rickshaw_graph .detail .item.active{opacity:1}.rickshaw_graph .detail .x_label{font-family:Arial,sans-serif;border-radius:3px;padding:6px;opacity:.5;border:1px solid #e0e0e0;font-size:12px;position:absolute;background:#fff;white-space:nowrap}.rickshaw_graph .detail .item{position:absolute;z-index:2;border-radius:3px;padding:.25em;font-size:12px;font-family:Arial,sans-serif;opacity:0;background:rgba(0,0,0,.4);color:#fff;border:1px solid rgba(0,0,0,.4);margin-left:1em;margin-top:-1em;white-space:nowrap}.rickshaw_graph .detail .item.active{opacity:1;background:rgba(0,0,0,.8)}.rickshaw_graph .detail .item:before{content:"\25c2";position:absolute;left:-.5em;color:rgba(0,0,0,.7);width:0}.rickshaw_graph .detail .dot{width:4px;height:4px;margin-left:-4px;margin-top:-3px;border-radius:5px;position:absolute;box-shadow:0 0 2px rgba(0,0,0,.6);background:#fff;border-width:2px;border-style:solid;display:none;background-clip:padding-box}.rickshaw_graph .detail .dot.active{display:block}.rickshaw_graph{position:relative}.rickshaw_graph svg{display:block;overflow:hidden}.rickshaw_graph .x_tick{position:absolute;top:0;bottom:0;width:0;border-left:1px dotted rgba(0,0,0,.2);pointer-events:none}.rickshaw_graph .x_tick .title{position:absolute;font-size:12px;font-family:Arial,sans-serif;opacity:.5;white-space:nowrap;margin-left:3px;bottom:1px}.rickshaw_annotation_timeline{height:1px;border-top:1px solid #e0e0e0;margin-top:10px;position:relative}.rickshaw_annotation_timeline .annotation{position:absolute;height:6px;width:6px;margin-left:-2px;top:-3px;border-radius:5px;background-color:rgba(0,0,0,.25)}.rickshaw_graph .annotation_line{position:absolute;top:0;bottom:-6px;width:0;border-left:2px solid rgba(0,0,0,.3);display:none}.rickshaw_graph .annotation_line.active{display:block}.rickshaw_graph .annotation_range{background:rgba(0,0,0,.1);display:none;position:absolute;top:0;bottom:-6px}.rickshaw_graph .annotation_range.active{display:block}.rickshaw_graph .annotation_range.active.offscreen{display:none}.rickshaw_annotation_timeline .annotation .content{background:#fff;color:#000;opacity:.9;padding:5px;box-shadow:0 0 2px rgba(0,0,0,.8);border-radius:3px;position:relative;z-index:20;font-size:12px;padding:6px 8px 8px;top:18px;left:-11px;width:160px;display:none;cursor:pointer}.rickshaw_annotation_timeline .annotation .content:before{content:"\25b2";position:absolute;top:-11px;color:#fff;text-shadow:0 -1px 1px rgba(0,0,0,.8)}.rickshaw_annotation_timeline .annotation.active,.rickshaw_annotation_timeline .annotation:hover{background-color:rgba(0,0,0,.8);cursor:none}.rickshaw_annotation_timeline .annotation .content:hover{z-index:50}.rickshaw_annotation_timeline .annotation.active .content{display:block}.rickshaw_annotation_timeline .annotation:hover .content{display:block;z-index:50}.rickshaw_graph .y_axis,.rickshaw_graph .x_axis_d3{fill:none}.rickshaw_graph .y_ticks .tick,.rickshaw_graph .x_ticks_d3 .tick{stroke:rgba(0,0,0,.16);stroke-width:2px;shape-rendering:crisp-edges;pointer-events:none}.rickshaw_graph .y_grid .tick,.rickshaw_graph .x_grid_d3 .tick{z-index:-1;stroke:rgba(0,0,0,.2);stroke-width:1px;stroke-dasharray:1 1}.rickshaw_graph .y_grid path,.rickshaw_graph .x_grid_d3 path{fill:none;stroke:none}.rickshaw_graph .y_ticks path,.rickshaw_graph .x_ticks_d3 path{fill:none;stroke:gray}.rickshaw_graph .y_ticks text,.rickshaw_graph .x_ticks_d3 text{opacity:.5;font-size:12px;pointer-events:none}.rickshaw_graph .x_tick.glow .title,.rickshaw_graph .y_ticks.glow text{fill:#000;color:#000;text-shadow:-1px 1px 0 rgba(255,255,255,.1),1px -1px 0 rgba(255,255,255,.1),1px 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1),0 -1px 0 rgba(255,255,255,.1),1px 0 0 rgba(255,255,255,.1),-1px 0 0 rgba(255,255,255,.1),-1px -1px 0 rgba(255,255,255,.1)}.rickshaw_graph .x_tick.inverse .title,.rickshaw_graph .y_ticks.inverse text{fill:#fff;color:#fff;text-shadow:-1px 1px 0 rgba(0,0,0,.8),1px -1px 0 rgba(0,0,0,.8),1px 1px 0 rgba(0,0,0,.8),0 1px 0 rgba(0,0,0,.8),0 -1px 0 rgba(0,0,0,.8),1px 0 0 rgba(0,0,0,.8),-1px 0 0 rgba(0,0,0,.8),-1px -1px 0 rgba(0,0,0,.8)}.rickshaw_legend{font-family:Arial;font-size:12px;color:#fff;background:#404040;display:inline-block;padding:12px 5px;border-radius:2px;position:relative}.rickshaw_legend:hover{z-index:10}.rickshaw_legend .swatch{width:10px;height:10px;border:1px solid rgba(0,0,0,.2)}.rickshaw_legend .line{clear:both;line-height:140%;padding-right:15px}.rickshaw_legend .line .swatch{display:inline-block;margin-right:3px;border-radius:2px}.rickshaw_legend .label{margin:0;white-space:nowrap;display:inline;font-size:inherit;background-color:transparent;color:inherit;font-weight:400;line-height:normal;padding:0;text-shadow:none}.rickshaw_legend .action:hover{opacity:.6}.rickshaw_legend .action{margin-right:.2em;font-size:10px;opacity:.2;cursor:pointer;font-size:14px}.rickshaw_legend .line.disabled{opacity:.4}.rickshaw_legend ul{list-style-type:none;margin:0;padding:0;margin:2px;cursor:pointer}.rickshaw_legend li{padding:0 0 0 2px;min-width:80px;white-space:nowrap}.rickshaw_legend li:hover{background:rgba(255,255,255,.08);border-radius:3px}.rickshaw_legend li:active{background:rgba(255,255,255,.2);border-radius:3px} \ No newline at end of file diff --git a/cat-home/src/main/webapp/css/slider.css b/cat-home/src/main/webapp/css/slider.css new file mode 100644 index 0000000000000000000000000000000000000000..b527aa8686ebaaff0d41a0aa34ed5738174af30c --- /dev/null +++ b/cat-home/src/main/webapp/css/slider.css @@ -0,0 +1,138 @@ +/*! + * Slider for Bootstrap + * + * Copyright 2012 Stefan Petre + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + */ +.slider { + display: inline-block; + vertical-align: middle; + position: relative; +} +.slider.slider-horizontal { + width: 210px; + height: 20px; +} +.slider.slider-horizontal .slider-track { + height: 10px; + width: 100%; + margin-top: -5px; + top: 50%; + left: 0; +} +.slider.slider-horizontal .slider-selection { + height: 100%; + top: 0; + bottom: 0; +} +.slider.slider-horizontal .slider-handle { + margin-left: -10px; + margin-top: -5px; +} +.slider.slider-horizontal .slider-handle.triangle { + border-width: 0 10px 10px 10px; + width: 0; + height: 0; + border-bottom-color: #0480be; + margin-top: 0; +} +.slider.slider-vertical { + height: 210px; + width: 20px; +} +.slider.slider-vertical .slider-track { + width: 10px; + height: 100%; + margin-left: -5px; + left: 50%; + top: 0; +} +.slider.slider-vertical .slider-selection { + width: 100%; + left: 0; + top: 0; + bottom: 0; +} +.slider.slider-vertical .slider-handle { + margin-left: -5px; + margin-top: -10px; +} +.slider.slider-vertical .slider-handle.triangle { + border-width: 10px 0 10px 10px; + width: 1px; + height: 1px; + border-left-color: #0480be; + margin-left: 0; +} +.slider input { + display: none; +} +.slider .tooltip-inner { + white-space: nowrap; +} +.slider-track { + position: absolute; + cursor: pointer; + background-color: #f7f7f7; + background-image: -moz-linear-gradient(top, #f5f5f5, #f9f9f9); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f5f5f5), to(#f9f9f9)); + background-image: -webkit-linear-gradient(top, #f5f5f5, #f9f9f9); + background-image: -o-linear-gradient(top, #f5f5f5, #f9f9f9); + background-image: linear-gradient(to bottom, #f5f5f5, #f9f9f9); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#fff9f9f9', GradientType=0); + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); + -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} +.slider-selection { + position: absolute; + background-color: #f7f7f7; + background-image: -moz-linear-gradient(top, #f9f9f9, #f5f5f5); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f9f9f9), to(#f5f5f5)); + background-image: -webkit-linear-gradient(top, #f9f9f9, #f5f5f5); + background-image: -o-linear-gradient(top, #f9f9f9, #f5f5f5); + background-image: linear-gradient(to bottom, #f9f9f9, #f5f5f5); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff9f9f9', endColorstr='#fff5f5f5', GradientType=0); + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + -moz-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} +.slider-handle { + position: absolute; + width: 20px; + height: 20px; + background-color: #0e90d2; + background-image: -moz-linear-gradient(top, #149bdf, #0480be); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#149bdf), to(#0480be)); + background-image: -webkit-linear-gradient(top, #149bdf, #0480be); + background-image: -o-linear-gradient(top, #149bdf, #0480be); + background-image: linear-gradient(to bottom, #149bdf, #0480be); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff149bdf', endColorstr='#ff0480be', GradientType=0); + -webkit-box-shadow: inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05); + -moz-box-shadow: inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05); + box-shadow: inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05); + opacity: 0.8; + border: 0px solid transparent; +} +.slider-handle.round { + -webkit-border-radius: 20px; + -moz-border-radius: 20px; + border-radius: 20px; +} +.slider-handle.triangle { + background: transparent none; +} \ No newline at end of file diff --git a/cat-home/src/main/webapp/js/Chart.js b/cat-home/src/main/webapp/js/Chart.js new file mode 100644 index 0000000000000000000000000000000000000000..ffbe16f3711316a2f43305a7f65c2773ed1e48fb --- /dev/null +++ b/cat-home/src/main/webapp/js/Chart.js @@ -0,0 +1,1426 @@ +/*! + * Chart.js + * http://chartjs.org/ + * + * Copyright 2013 Nick Downie + * Released under the MIT license + * https://github.com/nnnick/Chart.js/blob/master/LICENSE.md + */ + +//Define the global Chart Variable as a class. +window.Chart = function(context){ + + var chart = this; + + + //Easing functions adapted from Robert Penner's easing equations + //http://www.robertpenner.com/easing/ + + var animationOptions = { + linear : function (t){ + return t; + }, + easeInQuad: function (t) { + return t*t; + }, + easeOutQuad: function (t) { + return -1 *t*(t-2); + }, + easeInOutQuad: function (t) { + if ((t/=1/2) < 1) return 1/2*t*t; + return -1/2 * ((--t)*(t-2) - 1); + }, + easeInCubic: function (t) { + return t*t*t; + }, + easeOutCubic: function (t) { + return 1*((t=t/1-1)*t*t + 1); + }, + easeInOutCubic: function (t) { + if ((t/=1/2) < 1) return 1/2*t*t*t; + return 1/2*((t-=2)*t*t + 2); + }, + easeInQuart: function (t) { + return t*t*t*t; + }, + easeOutQuart: function (t) { + return -1 * ((t=t/1-1)*t*t*t - 1); + }, + easeInOutQuart: function (t) { + if ((t/=1/2) < 1) return 1/2*t*t*t*t; + return -1/2 * ((t-=2)*t*t*t - 2); + }, + easeInQuint: function (t) { + return 1*(t/=1)*t*t*t*t; + }, + easeOutQuint: function (t) { + return 1*((t=t/1-1)*t*t*t*t + 1); + }, + easeInOutQuint: function (t) { + if ((t/=1/2) < 1) return 1/2*t*t*t*t*t; + return 1/2*((t-=2)*t*t*t*t + 2); + }, + easeInSine: function (t) { + return -1 * Math.cos(t/1 * (Math.PI/2)) + 1; + }, + easeOutSine: function (t) { + return 1 * Math.sin(t/1 * (Math.PI/2)); + }, + easeInOutSine: function (t) { + return -1/2 * (Math.cos(Math.PI*t/1) - 1); + }, + easeInExpo: function (t) { + return (t==0) ? 1 : 1 * Math.pow(2, 10 * (t/1 - 1)); + }, + easeOutExpo: function (t) { + return (t==1) ? 1 : 1 * (-Math.pow(2, -10 * t/1) + 1); + }, + easeInOutExpo: function (t) { + if (t==0) return 0; + if (t==1) return 1; + if ((t/=1/2) < 1) return 1/2 * Math.pow(2, 10 * (t - 1)); + return 1/2 * (-Math.pow(2, -10 * --t) + 2); + }, + easeInCirc: function (t) { + if (t>=1) return t; + return -1 * (Math.sqrt(1 - (t/=1)*t) - 1); + }, + easeOutCirc: function (t) { + return 1 * Math.sqrt(1 - (t=t/1-1)*t); + }, + easeInOutCirc: function (t) { + if ((t/=1/2) < 1) return -1/2 * (Math.sqrt(1 - t*t) - 1); + return 1/2 * (Math.sqrt(1 - (t-=2)*t) + 1); + }, + easeInElastic: function (t) { + var s=1.70158;var p=0;var a=1; + if (t==0) return 0; if ((t/=1)==1) return 1; if (!p) p=1*.3; + if (a < Math.abs(1)) { a=1; var s=p/4; } + else var s = p/(2*Math.PI) * Math.asin (1/a); + return -(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*1-s)*(2*Math.PI)/p )); + }, + easeOutElastic: function (t) { + var s=1.70158;var p=0;var a=1; + if (t==0) return 0; if ((t/=1)==1) return 1; if (!p) p=1*.3; + if (a < Math.abs(1)) { a=1; var s=p/4; } + else var s = p/(2*Math.PI) * Math.asin (1/a); + return a*Math.pow(2,-10*t) * Math.sin( (t*1-s)*(2*Math.PI)/p ) + 1; + }, + easeInOutElastic: function (t) { + var s=1.70158;var p=0;var a=1; + if (t==0) return 0; if ((t/=1/2)==2) return 1; if (!p) p=1*(.3*1.5); + if (a < Math.abs(1)) { a=1; var s=p/4; } + else var s = p/(2*Math.PI) * Math.asin (1/a); + if (t < 1) return -.5*(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*1-s)*(2*Math.PI)/p )); + return a*Math.pow(2,-10*(t-=1)) * Math.sin( (t*1-s)*(2*Math.PI)/p )*.5 + 1; + }, + easeInBack: function (t) { + var s = 1.70158; + return 1*(t/=1)*t*((s+1)*t - s); + }, + easeOutBack: function (t) { + var s = 1.70158; + return 1*((t=t/1-1)*t*((s+1)*t + s) + 1); + }, + easeInOutBack: function (t) { + var s = 1.70158; + if ((t/=1/2) < 1) return 1/2*(t*t*(((s*=(1.525))+1)*t - s)); + return 1/2*((t-=2)*t*(((s*=(1.525))+1)*t + s) + 2); + }, + easeInBounce: function (t) { + return 1 - animationOptions.easeOutBounce (1-t); + }, + easeOutBounce: function (t) { + if ((t/=1) < (1/2.75)) { + return 1*(7.5625*t*t); + } else if (t < (2/2.75)) { + return 1*(7.5625*(t-=(1.5/2.75))*t + .75); + } else if (t < (2.5/2.75)) { + return 1*(7.5625*(t-=(2.25/2.75))*t + .9375); + } else { + return 1*(7.5625*(t-=(2.625/2.75))*t + .984375); + } + }, + easeInOutBounce: function (t) { + if (t < 1/2) return animationOptions.easeInBounce (t*2) * .5; + return animationOptions.easeOutBounce (t*2-1) * .5 + 1*.5; + } + }; + + //Variables global to the chart + var width = context.canvas.width; + var height = context.canvas.height; + + + //High pixel density displays - multiply the size of the canvas height/width by the device pixel ratio, then scale. + if (window.devicePixelRatio) { + context.canvas.style.width = width + "px"; + context.canvas.style.height = height + "px"; + context.canvas.height = height * window.devicePixelRatio; + context.canvas.width = width * window.devicePixelRatio; + context.scale(window.devicePixelRatio, window.devicePixelRatio); + } + + this.PolarArea = function(data,options){ + + chart.PolarArea.defaults = { + scaleOverlay : true, + scaleOverride : false, + scaleSteps : null, + scaleStepWidth : null, + scaleStartValue : null, + scaleShowLine : true, + scaleLineColor : "rgba(0,0,0,.1)", + scaleLineWidth : 1, + scaleShowLabels : true, + scaleLabel : "<%=value%>", + scaleFontFamily : "'Arial'", + scaleFontSize : 12, + scaleFontStyle : "normal", + scaleFontColor : "#666", + scaleShowLabelBackdrop : true, + scaleBackdropColor : "rgba(255,255,255,0.75)", + scaleBackdropPaddingY : 2, + scaleBackdropPaddingX : 2, + segmentShowStroke : true, + segmentStrokeColor : "#fff", + segmentStrokeWidth : 2, + animation : true, + animationSteps : 100, + animationEasing : "easeOutBounce", + animateRotate : true, + animateScale : false, + onAnimationComplete : null + }; + + var config = (options)? mergeChartConfig(chart.PolarArea.defaults,options) : chart.PolarArea.defaults; + + return new PolarArea(data,config,context); + }; + + this.Radar = function(data,options){ + + chart.Radar.defaults = { + scaleOverlay : false, + scaleOverride : false, + scaleSteps : null, + scaleStepWidth : null, + scaleStartValue : null, + scaleShowLine : true, + scaleLineColor : "rgba(0,0,0,.1)", + scaleLineWidth : 1, + scaleShowLabels : false, + scaleLabel : "<%=value%>", + scaleFontFamily : "'Arial'", + scaleFontSize : 12, + scaleFontStyle : "normal", + scaleFontColor : "#666", + scaleShowLabelBackdrop : true, + scaleBackdropColor : "rgba(255,255,255,0.75)", + scaleBackdropPaddingY : 2, + scaleBackdropPaddingX : 2, + angleShowLineOut : true, + angleLineColor : "rgba(0,0,0,.1)", + angleLineWidth : 1, + pointLabelFontFamily : "'Arial'", + pointLabelFontStyle : "normal", + pointLabelFontSize : 12, + pointLabelFontColor : "#666", + pointDot : true, + pointDotRadius : 3, + pointDotStrokeWidth : 1, + datasetStroke : true, + datasetStrokeWidth : 2, + datasetFill : true, + animation : true, + animationSteps : 60, + animationEasing : "easeOutQuart", + onAnimationComplete : null + }; + + var config = (options)? mergeChartConfig(chart.Radar.defaults,options) : chart.Radar.defaults; + + return new Radar(data,config,context); + }; + + this.Pie = function(data,options){ + chart.Pie.defaults = { + segmentShowStroke : true, + segmentStrokeColor : "#fff", + segmentStrokeWidth : 2, + animation : true, + animationSteps : 100, + animationEasing : "easeOutBounce", + animateRotate : true, + animateScale : false, + onAnimationComplete : null + }; + + var config = (options)? mergeChartConfig(chart.Pie.defaults,options) : chart.Pie.defaults; + + return new Pie(data,config,context); + }; + + this.Doughnut = function(data,options){ + + chart.Doughnut.defaults = { + segmentShowStroke : true, + segmentStrokeColor : "#fff", + segmentStrokeWidth : 2, + percentageInnerCutout : 50, + animation : true, + animationSteps : 100, + animationEasing : "easeOutBounce", + animateRotate : true, + animateScale : false, + onAnimationComplete : null + }; + + var config = (options)? mergeChartConfig(chart.Doughnut.defaults,options) : chart.Doughnut.defaults; + + return new Doughnut(data,config,context); + + }; + + this.Line = function(data,options){ + + chart.Line.defaults = { + scaleOverlay : false, + scaleOverride : false, + scaleSteps : null, + scaleStepWidth : null, + scaleStartValue : null, + scaleLineColor : "rgba(0,0,0,.1)", + scaleLineWidth : 1, + scaleShowLabels : true, + scaleLabel : "<%=value%>", + scaleFontFamily : "'Arial'", + scaleFontSize : 12, + scaleFontStyle : "normal", + scaleFontColor : "#666", + scaleShowGridLines : true, + scaleGridLineColor : "rgba(0,0,0,.05)", + scaleGridLineWidth : 1, + bezierCurve : true, + pointDot : true, + pointDotRadius : 4, + pointDotStrokeWidth : 2, + datasetStroke : true, + datasetStrokeWidth : 2, + datasetFill : true, + animation : true, + animationSteps : 60, + animationEasing : "easeOutQuart", + onAnimationComplete : null + }; + var config = (options) ? mergeChartConfig(chart.Line.defaults,options) : chart.Line.defaults; + + return new Line(data,config,context); + } + + this.Bar = function(data,options){ + chart.Bar.defaults = { + scaleOverlay : false, + scaleOverride : false, + scaleSteps : null, + scaleStepWidth : null, + scaleStartValue : null, + scaleLineColor : "rgba(0,0,0,.1)", + scaleLineWidth : 1, + scaleShowLabels : true, + scaleLabel : "<%=value%>", + scaleFontFamily : "'Arial'", + scaleFontSize : 12, + scaleFontStyle : "normal", + scaleFontColor : "#666", + scaleShowGridLines : true, + scaleGridLineColor : "rgba(0,0,0,.05)", + scaleGridLineWidth : 1, + barShowStroke : true, + barStrokeWidth : 2, + barValueSpacing : 5, + barDatasetSpacing : 1, + animation : true, + animationSteps : 60, + animationEasing : "easeOutQuart", + onAnimationComplete : null + }; + var config = (options) ? mergeChartConfig(chart.Bar.defaults,options) : chart.Bar.defaults; + + return new Bar(data,config,context); + } + + var clear = function(c){ + c.clearRect(0, 0, width, height); + }; + + var PolarArea = function(data,config,ctx){ + var maxSize, scaleHop, calculatedScale, labelHeight, scaleHeight, valueBounds, labelTemplateString; + + + calculateDrawingSizes(); + + valueBounds = getValueBounds(); + + labelTemplateString = (config.scaleShowLabels)? config.scaleLabel : null; + + //Check and set the scale + if (!config.scaleOverride){ + + calculatedScale = calculateScale(scaleHeight,valueBounds.maxSteps,valueBounds.minSteps,valueBounds.maxValue,valueBounds.minValue,labelTemplateString); + } + else { + calculatedScale = { + steps : config.scaleSteps, + stepValue : config.scaleStepWidth, + graphMin : config.scaleStartValue, + labels : [] + } + populateLabels(labelTemplateString, calculatedScale.labels,calculatedScale.steps,config.scaleStartValue,config.scaleStepWidth); + } + + scaleHop = maxSize/(calculatedScale.steps); + + //Wrap in an animation loop wrapper + animationLoop(config,drawScale,drawAllSegments,ctx); + + function calculateDrawingSizes(){ + maxSize = (Min([width,height])/2); + //Remove whatever is larger - the font size or line width. + + maxSize -= Max([config.scaleFontSize*0.5,config.scaleLineWidth*0.5]); + + labelHeight = config.scaleFontSize*2; + //If we're drawing the backdrop - add the Y padding to the label height and remove from drawing region. + if (config.scaleShowLabelBackdrop){ + labelHeight += (2 * config.scaleBackdropPaddingY); + maxSize -= config.scaleBackdropPaddingY*1.5; + } + + scaleHeight = maxSize; + //If the label height is less than 5, set it to 5 so we don't have lines on top of each other. + labelHeight = Default(labelHeight,5); + } + function drawScale(){ + for (var i=0; i upperValue) {upperValue = data[i].value;} + if (data[i].value < lowerValue) {lowerValue = data[i].value;} + }; + + var maxSteps = Math.floor((scaleHeight / (labelHeight*0.66))); + var minSteps = Math.floor((scaleHeight / labelHeight*0.5)); + + return { + maxValue : upperValue, + minValue : lowerValue, + maxSteps : maxSteps, + minSteps : minSteps + }; + + + } + } + + var Radar = function (data,config,ctx) { + var maxSize, scaleHop, calculatedScale, labelHeight, scaleHeight, valueBounds, labelTemplateString; + + //If no labels are defined set to an empty array, so referencing length for looping doesn't blow up. + if (!data.labels) data.labels = []; + + calculateDrawingSizes(); + + var valueBounds = getValueBounds(); + + labelTemplateString = (config.scaleShowLabels)? config.scaleLabel : null; + + //Check and set the scale + if (!config.scaleOverride){ + + calculatedScale = calculateScale(scaleHeight,valueBounds.maxSteps,valueBounds.minSteps,valueBounds.maxValue,valueBounds.minValue,labelTemplateString); + } + else { + calculatedScale = { + steps : config.scaleSteps, + stepValue : config.scaleStepWidth, + graphMin : config.scaleStartValue, + labels : [] + } + populateLabels(labelTemplateString, calculatedScale.labels,calculatedScale.steps,config.scaleStartValue,config.scaleStepWidth); + } + + scaleHop = maxSize/(calculatedScale.steps); + + animationLoop(config,drawScale,drawAllDataPoints,ctx); + + //Radar specific functions. + function drawAllDataPoints(animationDecimal){ + var rotationDegree = (2*Math.PI)/data.datasets[0].data.length; + + ctx.save(); + //translate to the centre of the canvas. + ctx.translate(width/2,height/2); + + //We accept multiple data sets for radar charts, so show loop through each set + for (var i=0; i Math.PI){ + ctx.textAlign = "right"; + } + else{ + ctx.textAlign = "left"; + } + + ctx.textBaseline = "middle"; + + ctx.fillText(data.labels[k],opposite,-adjacent); + + } + ctx.restore(); + }; + function calculateDrawingSizes(){ + maxSize = (Min([width,height])/2); + + labelHeight = config.scaleFontSize*2; + + var labelLength = 0; + for (var i=0; ilabelLength) labelLength = textMeasurement; + } + + //Figure out whats the largest - the height of the text or the width of what's there, and minus it from the maximum usable size. + maxSize -= Max([labelLength,((config.pointLabelFontSize/2)*1.5)]); + + maxSize -= config.pointLabelFontSize; + maxSize = CapValue(maxSize, null, 0); + scaleHeight = maxSize; + //If the label height is less than 5, set it to 5 so we don't have lines on top of each other. + labelHeight = Default(labelHeight,5); + }; + function getValueBounds() { + var upperValue = Number.MIN_VALUE; + var lowerValue = Number.MAX_VALUE; + + for (var i=0; i upperValue){upperValue = data.datasets[i].data[j]} + if (data.datasets[i].data[j] < lowerValue){lowerValue = data.datasets[i].data[j]} + } + } + + var maxSteps = Math.floor((scaleHeight / (labelHeight*0.66))); + var minSteps = Math.floor((scaleHeight / labelHeight*0.5)); + + return { + maxValue : upperValue, + minValue : lowerValue, + maxSteps : maxSteps, + minSteps : minSteps + }; + + + } + } + + var Pie = function(data,config,ctx){ + var segmentTotal = 0; + + //In case we have a canvas that is not a square. Minus 5 pixels as padding round the edge. + var pieRadius = Min([height/2,width/2]) - 5; + + for (var i=0; i 0){ + ctx.save(); + ctx.textAlign = "right"; + } + else{ + ctx.textAlign = "center"; + } + ctx.fillStyle = config.scaleFontColor; + for (var i=0; i 0){ + ctx.translate(yAxisPosX + i*valueHop,xAxisPosY + config.scaleFontSize); + ctx.rotate(-(rotateLabels * (Math.PI/180))); + ctx.fillText(data.labels[i], 0,0); + ctx.restore(); + } + + else{ + ctx.fillText(data.labels[i], yAxisPosX + i*valueHop,xAxisPosY + config.scaleFontSize+3); + } + + ctx.beginPath(); + ctx.moveTo(yAxisPosX + i * valueHop, xAxisPosY+3); + + //Check i isnt 0, so we dont go over the Y axis twice. + if(config.scaleShowGridLines && i>0){ + ctx.lineWidth = config.scaleGridLineWidth; + ctx.strokeStyle = config.scaleGridLineColor; + ctx.lineTo(yAxisPosX + i * valueHop, 5); + } + else{ + ctx.lineTo(yAxisPosX + i * valueHop, xAxisPosY+3); + } + ctx.stroke(); + } + + //Y axis + ctx.lineWidth = config.scaleLineWidth; + ctx.strokeStyle = config.scaleLineColor; + ctx.beginPath(); + ctx.moveTo(yAxisPosX,xAxisPosY+5); + ctx.lineTo(yAxisPosX,5); + ctx.stroke(); + + ctx.textAlign = "right"; + ctx.textBaseline = "middle"; + for (var j=0; j longestText)? measuredText : longestText; + } + //Add a little extra padding from the y axis + longestText +=10; + } + xAxisLength = width - longestText - widestXLabel; + valueHop = Math.floor(xAxisLength/(data.labels.length-1)); + + yAxisPosX = width-widestXLabel/2-xAxisLength; + xAxisPosY = scaleHeight + config.scaleFontSize/2; + } + function calculateDrawingSizes(){ + maxSize = height; + + //Need to check the X axis first - measure the length of each text metric, and figure out if we need to rotate by 45 degrees. + ctx.font = config.scaleFontStyle + " " + config.scaleFontSize+"px " + config.scaleFontFamily; + widestXLabel = 1; + for (var i=0; i widestXLabel)? textLength : widestXLabel; + } + if (width/data.labels.length < widestXLabel){ + rotateLabels = 45; + if (width/data.labels.length < Math.cos(rotateLabels) * widestXLabel){ + rotateLabels = 90; + maxSize -= widestXLabel; + } + else{ + maxSize -= Math.sin(rotateLabels) * widestXLabel; + } + } + else{ + maxSize -= config.scaleFontSize; + } + + //Add a little padding between the x line and the text + maxSize -= 5; + + + labelHeight = config.scaleFontSize; + + maxSize -= labelHeight; + //Set 5 pixels greater than the font size to allow for a little padding from the X axis. + + scaleHeight = maxSize; + + //Then get the area above we can safely draw on. + + } + function getValueBounds() { + var upperValue = Number.MIN_VALUE; + var lowerValue = Number.MAX_VALUE; + for (var i=0; i upperValue) { upperValue = data.datasets[i].data[j] }; + if ( data.datasets[i].data[j] < lowerValue) { lowerValue = data.datasets[i].data[j] }; + } + }; + + var maxSteps = Math.floor((scaleHeight / (labelHeight*0.66))); + var minSteps = Math.floor((scaleHeight / labelHeight*0.5)); + + return { + maxValue : upperValue, + minValue : lowerValue, + maxSteps : maxSteps, + minSteps : minSteps + }; + + + } + + + } + + var Bar = function(data,config,ctx){ + var maxSize, scaleHop, calculatedScale, labelHeight, scaleHeight, valueBounds, labelTemplateString, valueHop,widestXLabel, xAxisLength,yAxisPosX,xAxisPosY,barWidth, rotateLabels = 0; + + calculateDrawingSizes(); + + valueBounds = getValueBounds(); + //Check and set the scale + labelTemplateString = (config.scaleShowLabels)? config.scaleLabel : ""; + if (!config.scaleOverride){ + + calculatedScale = calculateScale(scaleHeight,valueBounds.maxSteps,valueBounds.minSteps,valueBounds.maxValue,valueBounds.minValue,labelTemplateString); + } + else { + calculatedScale = { + steps : config.scaleSteps, + stepValue : config.scaleStepWidth, + graphMin : config.scaleStartValue, + labels : [] + } + populateLabels(labelTemplateString, calculatedScale.labels,calculatedScale.steps,config.scaleStartValue,config.scaleStepWidth); + } + + scaleHop = Math.floor(scaleHeight/calculatedScale.steps); + calculateXAxisSize(); + animationLoop(config,drawScale,drawBars,ctx); + + function drawBars(animPc){ + ctx.lineWidth = config.barStrokeWidth; + for (var i=0; i 0){ + ctx.save(); + ctx.textAlign = "right"; + } + else{ + ctx.textAlign = "center"; + } + ctx.fillStyle = config.scaleFontColor; + for (var i=0; i 0){ + ctx.translate(yAxisPosX + i*valueHop,xAxisPosY + config.scaleFontSize); + ctx.rotate(-(rotateLabels * (Math.PI/180))); + ctx.fillText(data.labels[i], 0,0); + ctx.restore(); + } + + else{ + ctx.fillText(data.labels[i], yAxisPosX + i*valueHop + valueHop/2,xAxisPosY + config.scaleFontSize+3); + } + + ctx.beginPath(); + ctx.moveTo(yAxisPosX + (i+1) * valueHop, xAxisPosY+3); + + //Check i isnt 0, so we dont go over the Y axis twice. + ctx.lineWidth = config.scaleGridLineWidth; + ctx.strokeStyle = config.scaleGridLineColor; + ctx.lineTo(yAxisPosX + (i+1) * valueHop, 5); + ctx.stroke(); + } + + //Y axis + ctx.lineWidth = config.scaleLineWidth; + ctx.strokeStyle = config.scaleLineColor; + ctx.beginPath(); + ctx.moveTo(yAxisPosX,xAxisPosY+5); + ctx.lineTo(yAxisPosX,5); + ctx.stroke(); + + ctx.textAlign = "right"; + ctx.textBaseline = "middle"; + for (var j=0; j longestText)? measuredText : longestText; + } + //Add a little extra padding from the y axis + longestText +=10; + } + xAxisLength = width - longestText - widestXLabel; + valueHop = Math.floor(xAxisLength/(data.labels.length)); + + barWidth = (valueHop - config.scaleGridLineWidth*2 - (config.barValueSpacing*2) - (config.barDatasetSpacing*data.datasets.length-1) - ((config.barStrokeWidth/2)*data.datasets.length-1))/data.datasets.length; + + yAxisPosX = width-widestXLabel/2-xAxisLength; + xAxisPosY = scaleHeight + config.scaleFontSize/2; + } + function calculateDrawingSizes(){ + maxSize = height; + + //Need to check the X axis first - measure the length of each text metric, and figure out if we need to rotate by 45 degrees. + ctx.font = config.scaleFontStyle + " " + config.scaleFontSize+"px " + config.scaleFontFamily; + widestXLabel = 1; + for (var i=0; i widestXLabel)? textLength : widestXLabel; + } + if (width/data.labels.length < widestXLabel){ + rotateLabels = 45; + if (width/data.labels.length < Math.cos(rotateLabels) * widestXLabel){ + rotateLabels = 90; + maxSize -= widestXLabel; + } + else{ + maxSize -= Math.sin(rotateLabels) * widestXLabel; + } + } + else{ + maxSize -= config.scaleFontSize; + } + + //Add a little padding between the x line and the text + maxSize -= 5; + + + labelHeight = config.scaleFontSize; + + maxSize -= labelHeight; + //Set 5 pixels greater than the font size to allow for a little padding from the X axis. + + scaleHeight = maxSize; + + //Then get the area above we can safely draw on. + + } + function getValueBounds() { + var upperValue = Number.MIN_VALUE; + var lowerValue = Number.MAX_VALUE; + for (var i=0; i upperValue) { upperValue = data.datasets[i].data[j] }; + if ( data.datasets[i].data[j] < lowerValue) { lowerValue = data.datasets[i].data[j] }; + } + }; + + var maxSteps = Math.floor((scaleHeight / (labelHeight*0.66))); + var minSteps = Math.floor((scaleHeight / labelHeight*0.5)); + + return { + maxValue : upperValue, + minValue : lowerValue, + maxSteps : maxSteps, + minSteps : minSteps + }; + + + } + } + + function calculateOffset(val,calculatedScale,scaleHop){ + var outerValue = calculatedScale.steps * calculatedScale.stepValue; + var adjustedValue = val - calculatedScale.graphMin; + var scalingFactor = CapValue(adjustedValue/outerValue,1,0); + return (scaleHop*calculatedScale.steps) * scalingFactor; + } + + function animationLoop(config,drawScale,drawData,ctx){ + var animFrameAmount = (config.animation)? 1/CapValue(config.animationSteps,Number.MAX_VALUE,1) : 1, + easingFunction = animationOptions[config.animationEasing], + percentAnimComplete =(config.animation)? 0 : 1; + + + + if (typeof drawScale !== "function") drawScale = function(){}; + + requestAnimFrame(animLoop); + + function animateFrame(){ + var easeAdjustedAnimationPercent =(config.animation)? CapValue(easingFunction(percentAnimComplete),null,0) : 1; + clear(ctx); + if(config.scaleOverlay){ + drawData(easeAdjustedAnimationPercent); + drawScale(); + } else { + drawScale(); + drawData(easeAdjustedAnimationPercent); + } + } + function animLoop(){ + //We need to check if the animation is incomplete (less than 1), or complete (1). + percentAnimComplete += animFrameAmount; + animateFrame(); + //Stop the loop continuing forever + if (percentAnimComplete <= 1){ + requestAnimFrame(animLoop); + } + else{ + if (typeof config.onAnimationComplete == "function") config.onAnimationComplete(); + } + + } + + } + + //Declare global functions to be called within this namespace here. + + + // shim layer with setTimeout fallback + var requestAnimFrame = (function(){ + return window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + window.oRequestAnimationFrame || + window.msRequestAnimationFrame || + function(callback) { + window.setTimeout(callback, 1000 / 60); + }; + })(); + + function calculateScale(drawingHeight,maxSteps,minSteps,maxValue,minValue,labelTemplateString){ + var graphMin,graphMax,graphRange,stepValue,numberOfSteps,valueRange,rangeOrderOfMagnitude,decimalNum; + + valueRange = maxValue - minValue; + + rangeOrderOfMagnitude = calculateOrderOfMagnitude(valueRange); + + graphMin = Math.floor(minValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude); + + graphMax = Math.ceil(maxValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude); + + graphRange = graphMax - graphMin; + + stepValue = Math.pow(10, rangeOrderOfMagnitude); + + numberOfSteps = Math.round(graphRange / stepValue); + + //Compare number of steps to the max and min for that size graph, and add in half steps if need be. + while(numberOfSteps < minSteps || numberOfSteps > maxSteps) { + if (numberOfSteps < minSteps){ + stepValue /= 2; + numberOfSteps = Math.round(graphRange/stepValue); + } + else{ + stepValue *=2; + numberOfSteps = Math.round(graphRange/stepValue); + } + }; + + var labels = []; + populateLabels(labelTemplateString, labels, numberOfSteps, graphMin, stepValue); + + return { + steps : numberOfSteps, + stepValue : stepValue, + graphMin : graphMin, + labels : labels + + } + + function calculateOrderOfMagnitude(val){ + return Math.floor(Math.log(val) / Math.LN10); + } + + + } + + //Populate an array of all the labels by interpolating the string. + function populateLabels(labelTemplateString, labels, numberOfSteps, graphMin, stepValue) { + if (labelTemplateString) { + //Fix floating point errors by setting to fixed the on the same decimal as the stepValue. + for (var i = 1; i < numberOfSteps + 1; i++) { + labels.push(tmpl(labelTemplateString, {value: (graphMin + (stepValue * i)).toFixed(getDecimalPlaces(stepValue))})); + } + } + } + + //Max value from array + function Max( array ){ + return Math.max.apply( Math, array ); + }; + //Min value from array + function Min( array ){ + return Math.min.apply( Math, array ); + }; + //Default if undefined + function Default(userDeclared,valueIfFalse){ + if(!userDeclared){ + return valueIfFalse; + } else { + return userDeclared; + } + }; + //Is a number function + function isNumber(n) { + return !isNaN(parseFloat(n)) && isFinite(n); + } + //Apply cap a value at a high or low number + function CapValue(valueToCap, maxValue, minValue){ + if(isNumber(maxValue)) { + if( valueToCap > maxValue ) { + return maxValue; + } + } + if(isNumber(minValue)){ + if ( valueToCap < minValue ){ + return minValue; + } + } + return valueToCap; + } + function getDecimalPlaces (num){ + var numberOfDecimalPlaces; + if (num%1!=0){ + return num.toString().split(".")[1].length + } + else{ + return 0; + } + + } + + function mergeChartConfig(defaults,userDefined){ + var returnObj = {}; + for (var attrname in defaults) { returnObj[attrname] = defaults[attrname]; } + for (var attrname in userDefined) { returnObj[attrname] = userDefined[attrname]; } + return returnObj; + } + + //Javascript micro templating by John Resig - source at http://ejohn.org/blog/javascript-micro-templating/ + var cache = {}; + + function tmpl(str, data){ + // Figure out if we're getting a template, or if we need to + // load the template - and be sure to cache the result. + var fn = !/\W/.test(str) ? + cache[str] = cache[str] || + tmpl(document.getElementById(str).innerHTML) : + + // Generate a reusable function that will serve as a template + // generator (and which will be cached). + new Function("obj", + "var p=[],print=function(){p.push.apply(p,arguments);};" + + + // Introduce the data as local variables using with(){} + "with(obj){p.push('" + + + // Convert the template into pure JavaScript + str + .replace(/[\r\t\n]/g, " ") + .split("<%").join("\t") + .replace(/((^|%>)[^\t]*)'/g, "$1\r") + .replace(/\t=(.*?)%>/g, "',$1,'") + .split("\t").join("');") + .split("%>").join("p.push('") + .split("\r").join("\\'") + + "');}return p.join('');"); + + // Provide some basic currying to the user + return data ? fn( data ) : fn; + }; +} + + diff --git a/cat-home/src/main/webapp/js/Chart.min.js b/cat-home/src/main/webapp/js/Chart.min.js new file mode 100644 index 0000000000000000000000000000000000000000..ab63588108763378ebc76ea1bb73191a70d70b12 --- /dev/null +++ b/cat-home/src/main/webapp/js/Chart.min.js @@ -0,0 +1,39 @@ +var Chart=function(s){function v(a,c,b){a=A((a-c.graphMin)/(c.steps*c.stepValue),1,0);return b*c.steps*a}function x(a,c,b,e){function h(){g+=f;var k=a.animation?A(d(g),null,0):1;e.clearRect(0,0,q,u);a.scaleOverlay?(b(k),c()):(c(),b(k));if(1>=g)D(h);else if("function"==typeof a.onAnimationComplete)a.onAnimationComplete()}var f=a.animation?1/A(a.animationSteps,Number.MAX_VALUE,1):1,d=B[a.animationEasing],g=a.animation?0:1;"function"!==typeof c&&(c=function(){});D(h)}function C(a,c,b,e,h,f){var d;a= +Math.floor(Math.log(e-h)/Math.LN10);h=Math.floor(h/(1*Math.pow(10,a)))*Math.pow(10,a);e=Math.ceil(e/(1*Math.pow(10,a)))*Math.pow(10,a)-h;a=Math.pow(10,a);for(d=Math.round(e/a);dc;)a=dc?c:!isNaN(parseFloat(b))&& +isFinite(b)&&a)[^\t]*)'/g,"$1\r").replace(/\t=(.*?)%>/g,"',$1,'").split("\t").join("');").split("%>").join("p.push('").split("\r").join("\\'")+"');}return p.join('');");return c? +b(c):b}var r=this,B={linear:function(a){return a},easeInQuad:function(a){return a*a},easeOutQuad:function(a){return-1*a*(a-2)},easeInOutQuad:function(a){return 1>(a/=0.5)?0.5*a*a:-0.5*(--a*(a-2)-1)},easeInCubic:function(a){return a*a*a},easeOutCubic:function(a){return 1*((a=a/1-1)*a*a+1)},easeInOutCubic:function(a){return 1>(a/=0.5)?0.5*a*a*a:0.5*((a-=2)*a*a+2)},easeInQuart:function(a){return a*a*a*a},easeOutQuart:function(a){return-1*((a=a/1-1)*a*a*a-1)},easeInOutQuart:function(a){return 1>(a/=0.5)? +0.5*a*a*a*a:-0.5*((a-=2)*a*a*a-2)},easeInQuint:function(a){return 1*(a/=1)*a*a*a*a},easeOutQuint:function(a){return 1*((a=a/1-1)*a*a*a*a+1)},easeInOutQuint:function(a){return 1>(a/=0.5)?0.5*a*a*a*a*a:0.5*((a-=2)*a*a*a*a+2)},easeInSine:function(a){return-1*Math.cos(a/1*(Math.PI/2))+1},easeOutSine:function(a){return 1*Math.sin(a/1*(Math.PI/2))},easeInOutSine:function(a){return-0.5*(Math.cos(Math.PI*a/1)-1)},easeInExpo:function(a){return 0==a?1:1*Math.pow(2,10*(a/1-1))},easeOutExpo:function(a){return 1== +a?1:1*(-Math.pow(2,-10*a/1)+1)},easeInOutExpo:function(a){return 0==a?0:1==a?1:1>(a/=0.5)?0.5*Math.pow(2,10*(a-1)):0.5*(-Math.pow(2,-10*--a)+2)},easeInCirc:function(a){return 1<=a?a:-1*(Math.sqrt(1-(a/=1)*a)-1)},easeOutCirc:function(a){return 1*Math.sqrt(1-(a=a/1-1)*a)},easeInOutCirc:function(a){return 1>(a/=0.5)?-0.5*(Math.sqrt(1-a*a)-1):0.5*(Math.sqrt(1-(a-=2)*a)+1)},easeInElastic:function(a){var c=1.70158,b=0,e=1;if(0==a)return 0;if(1==(a/=1))return 1;b||(b=0.3);ea?-0.5*e*Math.pow(2,10* +(a-=1))*Math.sin((1*a-c)*2*Math.PI/b):0.5*e*Math.pow(2,-10*(a-=1))*Math.sin((1*a-c)*2*Math.PI/b)+1},easeInBack:function(a){return 1*(a/=1)*a*(2.70158*a-1.70158)},easeOutBack:function(a){return 1*((a=a/1-1)*a*(2.70158*a+1.70158)+1)},easeInOutBack:function(a){var c=1.70158;return 1>(a/=0.5)?0.5*a*a*(((c*=1.525)+1)*a-c):0.5*((a-=2)*a*(((c*=1.525)+1)*a+c)+2)},easeInBounce:function(a){return 1-B.easeOutBounce(1-a)},easeOutBounce:function(a){return(a/=1)<1/2.75?1*7.5625*a*a:a<2/2.75?1*(7.5625*(a-=1.5/2.75)* +a+0.75):a<2.5/2.75?1*(7.5625*(a-=2.25/2.75)*a+0.9375):1*(7.5625*(a-=2.625/2.75)*a+0.984375)},easeInOutBounce:function(a){return 0.5>a?0.5*B.easeInBounce(2*a):0.5*B.easeOutBounce(2*a-1)+0.5}},q=s.canvas.width,u=s.canvas.height;window.devicePixelRatio&&(s.canvas.style.width=q+"px",s.canvas.style.height=u+"px",s.canvas.height=u*window.devicePixelRatio,s.canvas.width=q*window.devicePixelRatio,s.scale(window.devicePixelRatio,window.devicePixelRatio));this.PolarArea=function(a,c){r.PolarArea.defaults={scaleOverlay:!0, +scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleShowLine:!0,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowLabelBackdrop:!0,scaleBackdropColor:"rgba(255,255,255,0.75)",scaleBackdropPaddingY:2,scaleBackdropPaddingX:2,segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,animation:!0,animationSteps:100,animationEasing:"easeOutBounce", +animateRotate:!0,animateScale:!1,onAnimationComplete:null};var b=c?y(r.PolarArea.defaults,c):r.PolarArea.defaults;return new G(a,b,s)};this.Radar=function(a,c){r.Radar.defaults={scaleOverlay:!1,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleShowLine:!0,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!1,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowLabelBackdrop:!0,scaleBackdropColor:"rgba(255,255,255,0.75)", +scaleBackdropPaddingY:2,scaleBackdropPaddingX:2,angleShowLineOut:!0,angleLineColor:"rgba(0,0,0,.1)",angleLineWidth:1,pointLabelFontFamily:"'Arial'",pointLabelFontStyle:"normal",pointLabelFontSize:12,pointLabelFontColor:"#666",pointDot:!0,pointDotRadius:3,pointDotStrokeWidth:1,datasetStroke:!0,datasetStrokeWidth:2,datasetFill:!0,animation:!0,animationSteps:60,animationEasing:"easeOutQuart",onAnimationComplete:null};var b=c?y(r.Radar.defaults,c):r.Radar.defaults;return new H(a,b,s)};this.Pie=function(a, +c){r.Pie.defaults={segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,animation:!0,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1,onAnimationComplete:null};var b=c?y(r.Pie.defaults,c):r.Pie.defaults;return new I(a,b,s)};this.Doughnut=function(a,c){r.Doughnut.defaults={segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,percentageInnerCutout:50,animation:!0,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1, +onAnimationComplete:null};var b=c?y(r.Doughnut.defaults,c):r.Doughnut.defaults;return new J(a,b,s)};this.Line=function(a,c){r.Line.defaults={scaleOverlay:!1,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,bezierCurve:!0, +pointDot:!0,pointDotRadius:4,pointDotStrokeWidth:2,datasetStroke:!0,datasetStrokeWidth:2,datasetFill:!0,animation:!0,animationSteps:60,animationEasing:"easeOutQuart",onAnimationComplete:null};var b=c?y(r.Line.defaults,c):r.Line.defaults;return new K(a,b,s)};this.Bar=function(a,c){r.Bar.defaults={scaleOverlay:!1,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'", +scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,barShowStroke:!0,barStrokeWidth:2,barValueSpacing:5,barDatasetSpacing:1,animation:!0,animationSteps:60,animationEasing:"easeOutQuart",onAnimationComplete:null};var b=c?y(r.Bar.defaults,c):r.Bar.defaults;return new L(a,b,s)};var G=function(a,c,b){var e,h,f,d,g,k,j,l,m;g=Math.min.apply(Math,[q,u])/2;g-=Math.max.apply(Math,[0.5*c.scaleFontSize,0.5*c.scaleLineWidth]); +d=2*c.scaleFontSize;c.scaleShowLabelBackdrop&&(d+=2*c.scaleBackdropPaddingY,g-=1.5*c.scaleBackdropPaddingY);l=g;d=d?d:5;e=Number.MIN_VALUE;h=Number.MAX_VALUE;for(f=0;fe&&(e=a[f].value),a[f].valuel&&(l=h);g-=Math.max.apply(Math,[l,1.5*(c.pointLabelFontSize/2)]);g-=c.pointLabelFontSize;l=g=A(g,null,0);d=d?d:5;e=Number.MIN_VALUE; +h=Number.MAX_VALUE;for(f=0;fe&&(e=a.datasets[f].data[m]),a.datasets[f].data[m]Math.PI?"right":"left";b.textBaseline="middle";b.fillText(a.labels[d],f,-h)}b.restore()},function(d){var e=2*Math.PI/a.datasets[0].data.length;b.save();b.translate(q/2,u/2);for(var g=0;gt?e:t;q/a.labels.lengthe&&(e=a.datasets[f].data[l]),a.datasets[f].data[l]d?h:d;d+=10}r=q-d-t;m=Math.floor(r/(a.labels.length-1));n=q-t/2-r;p=g+c.scaleFontSize/2;x(c,function(){b.lineWidth=c.scaleLineWidth;b.strokeStyle=c.scaleLineColor;b.beginPath();b.moveTo(q-t/2+5,p);b.lineTo(q-t/2-r-5,p);b.stroke();0t?e:t;q/a.labels.lengthe&&(e=a.datasets[f].data[l]),a.datasets[f].data[l]< +h&&(h=a.datasets[f].data[l]);f=Math.floor(g/(0.66*d));d=Math.floor(0.5*(g/d));l=c.scaleShowLabels?c.scaleLabel:"";c.scaleOverride?(j={steps:c.scaleSteps,stepValue:c.scaleStepWidth,graphMin:c.scaleStartValue,labels:[]},z(l,j.labels,j.steps,c.scaleStartValue,c.scaleStepWidth)):j=C(g,f,d,e,h,l);k=Math.floor(g/j.steps);d=1;if(c.scaleShowLabels){b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily;for(e=0;ed?h:d;d+=10}r=q-d-t;m= +Math.floor(r/a.labels.length);s=(m-2*c.scaleGridLineWidth-2*c.barValueSpacing-(c.barDatasetSpacing*a.datasets.length-1)-(c.barStrokeWidth/2*a.datasets.length-1))/a.datasets.length;n=q-t/2-r;p=g+c.scaleFontSize/2;x(c,function(){b.lineWidth=c.scaleLineWidth;b.strokeStyle=c.scaleLineColor;b.beginPath();b.moveTo(q-t/2+5,p);b.lineTo(q-t/2-r-5,p);b.stroke();0Platform' + + ''; + + innerHTML += ''; + + innerHTML += ''; + innerHTML += ''; + innerHTML += ''; + + return innerHTML; +} - $("#btnResume") - .click( - function() { - var checkbox = $(".table input[type='checkbox']:checked"); - var id = ""; - for ( var i = 0; i < checkbox.length; i++) { - id = id + $(":nth-child(2)",$(checkbox[i]).closest('tr')).html() + "-"; - } - url = window.location.href; - index = url.indexOf("&suspend"); - if (index != -1) { - window.location.href = url - .substring(0, index) - + "&suspend=1&ids=" + id; - } else { - if (url.indexOf("?") == -1) { - url = url + '?'; - } - window.location.href = url - + "&suspend=1&ids=" + id; - } - loaction.reload(); - }); +function insertOperator(){ + var innerHTML = ""; + innerHTML += ''; + innerHTML += ''; + innerHTML += ''; + innerHTML += ''; + innerHTML += ''; + + return innerHTML; +} - }); +function getConditionsHTML(){ + var conditions = ""; + + var trs = $('#div2 table tr'); + var size = trs.size(); + + for(var index = 0 ; index < size ; index++ ){ + var tr = trs.get(index); + var selects = $('select',tr); + var input = $('input',tr); + if(index != (size - 1)){ + var secondTr = trs.get(++index); + var op = $('.btn-primary',secondTr).text(); + conditions += "

" + $(selects[0]).val() + " " + $('option:selected',selects[1]).text() + " " + $(input).val() + " " + op + "

"; + }else{ + conditions += "

" + $(selects[0]).val() + " " + $('option:selected',selects[1]).text() + " " + $(input).val() + "

"; + } + } + + return conditions; +} + +function getConditions(){ + var conditions = []; + + $('input[type="url"]').each(function(i){ + var name = $(this).attr("name"); + + if(name && $(this).val()){ + var op = "and"; + if(name == "1"){ + op = "or"; + } + var condition = getCondition("url",op,name,$(this).val(),i); + conditions.push(condition); + } + + }); + + var trs = $('#div2 table tr'); + var size = trs.size(); + + for(var index = 0 ; index < size ; index++ ){ + var tr = trs.get(index); + var selects = $('select',tr); + var input = $('input',tr); + if(index != (size - 1)){ + var secondTr = trs.get(++index); + var tmpOp = $('.btn-primary',secondTr).text(); + var op = ""; + if(tmpOp == "AND"){ + op = "and"; + }else if(tmpOp == "OR"){ + op = "or"; + } + var condition = getCondition($(selects[0]).val(),op,$(selects[1]).val(),$(input).val(),3); + conditions.push(condition); + }else{ + var condition = getCondition($(selects[0]).val(),"and",$(selects[1]).val(),$(input).val(),3); + conditions.push(condition); + } + } + + var condition = getCondition("percent","and",1,$(".add-on:eq(3)").text(),4); + conditions.push(condition); + + if(conditions.length == 2){ + var firstCondition = conditions[0]; + + firstCondition["operator"] = "and"; + } + return conditions; +} + +function getCondition(name,operator,comparator,text,seq){ + var condition = { + "name":name, + "operator":operator, + "comparator":comparator, + "text":text, + "seq":seq + } + + return condition; +} + +function getConvertionGoals(){ + var goals = []; + + $('#div3 div').each(function(){ + var name = $("input:eq(0)",$(this)); + var url = $("input:eq(1)",$(this)); + + var goal = { + "name":name.val(), + "text":url.val() + }; + + goals.push(goal); + }); + + return goals; +} + +$(document).ready(function() { + $('#ckall').click(function() { + if ($('#ckall').is(':checked')) { + $(".table input[type='checkbox']").prop( + 'checked', true); + } else { + $(".table input[type='checkbox']").prop( + 'checked', false); + } + }); + + $("#btnSuspend").click(function() { + var checkbox = $(".table input[type='checkbox']:checked"); + var id = ""; + for ( var i = 0; i < checkbox.length; i++) { + id = id + $(":nth-child(2)",$(checkbox[i]).closest('tr')).html() + "-"; + } + url = window.location.href; + index = url.indexOf("&suspend"); + if (index != -1) { + window.location.href = url + .substring(0, index) + + "&suspend=-1&ids=" + id; + } else { + if (url.indexOf("?") == -1) { + url = url + '?'; + } + window.location.href = url + + "&suspend=-1&ids=" + id; + } + loaction.reload(); + }); + + $("#btnResume").click(function() { + var checkbox = $(".table input[type='checkbox']:checked"); + var id = ""; + for ( var i = 0; i < checkbox.length; i++) { + id = id + $(":nth-child(2)",$(checkbox[i]).closest('tr')).html() + "-"; + } + url = window.location.href; + index = url.indexOf("&suspend"); + if (index != -1) { + window.location.href = url + .substring(0, index) + + "&suspend=1&ids=" + id; + } else { + if (url.indexOf("?") == -1) { + url = url + '?'; + } + window.location.href = url + + "&suspend=1&ids=" + id; + } + loaction.reload(); + }); + + $('#addVistorCondition').click(function(){ + var tr = $(insertTr()); + $('a',tr).click(function(){ + var index = $(this).closest("tr").index(); + if(index > 0){ + $(this).closest("tr").prev().remove(); + $(this).closest("tr").remove(); + }else if(index == 0){ + $(this).closest("tr").next().remove(); + $(this).closest("tr").remove(); + } + }); + + if($('#div2 tbody tr:last').size() == 0){ + $('#div2 tbody').html(tr); + }else{ + var innerHTML = $(insertOperator()); + $('a',innerHTML).click(function(){ + $('a',innerHTML).toggleClass("btn-primary"); + }); + $('#div2 tbody tr:last').after(innerHTML); + $('#div2 tbody tr:last').after(tr); + } + }); + + var goalCounter = 1; + $('#addConvertionGoal').click(function(e){ + var innerHtml = []; + + innerHtml.push('
'); + innerHtml.push(''); + innerHtml.push(' triggers when visitor '); + innerHtml.push(''); + innerHtml.push(''); + innerHtml.push('
'); + + var line = $(innerHtml.join("")); + $('a',line).click(function(){ + $(this).closest("div").remove(); + }); + + $('#div3').append(line); + goalCounter += 1; + }); + +}); (function(w) { var abtest = { diff --git a/cat-home/src/main/webapp/js/bootstrap-slider.js b/cat-home/src/main/webapp/js/bootstrap-slider.js new file mode 100644 index 0000000000000000000000000000000000000000..f0c525abc3a00310dc5a3c67e3f93c8dcb644859 --- /dev/null +++ b/cat-home/src/main/webapp/js/bootstrap-slider.js @@ -0,0 +1,391 @@ +/* ========================================================= + * bootstrap-slider.js v2.0.0 + * http://www.eyecon.ro/bootstrap-slider + * ========================================================= + * Copyright 2012 Stefan Petre + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================= */ + +!function( $ ) { + + var Slider = function(element, options) { + this.element = $(element); + this.picker = $('
'+ + '
'+ + '
'+ + '
'+ + '
'+ + '
'+ + '
'+ + '
') + .insertBefore(this.element) + .append(this.element); + this.id = this.element.data('slider-id')||options.id; + if (this.id) { + this.picker[0].id = this.id; + } + + if (typeof Modernizr !== 'undefined' && Modernizr.touch) { + this.touchCapable = true; + } + + var tooltip = this.element.data('slider-tooltip')||options.tooltip; + + this.tooltip = this.picker.find('.tooltip'); + this.tooltipInner = this.tooltip.find('div.tooltip-inner'); + + this.orientation = this.element.data('slider-orientation')||options.orientation; + switch(this.orientation) { + case 'vertical': + this.picker.addClass('slider-vertical'); + this.stylePos = 'top'; + this.mousePos = 'pageY'; + this.sizePos = 'offsetHeight'; + this.tooltip.addClass('right')[0].style.left = '100%'; + break; + default: + this.picker + .addClass('slider-horizontal') + .css('width', this.element.outerWidth()); + this.orientation = 'horizontal'; + this.stylePos = 'left'; + this.mousePos = 'pageX'; + this.sizePos = 'offsetWidth'; + this.tooltip.addClass('top')[0].style.top = -this.tooltip.outerHeight() - 14 + 'px'; + break; + } + + this.min = this.element.data('slider-min')||options.min; + this.max = this.element.data('slider-max')||options.max; + this.step = this.element.data('slider-step')||options.step; + this.value = this.element.data('slider-value')||options.value; + if (this.value[1]) { + this.range = true; + } + + this.selection = this.element.data('slider-selection')||options.selection; + this.selectionEl = this.picker.find('.slider-selection'); + if (this.selection === 'none') { + this.selectionEl.addClass('hide'); + } + this.selectionElStyle = this.selectionEl[0].style; + + + this.handle1 = this.picker.find('.slider-handle:first'); + this.handle1Stype = this.handle1[0].style; + this.handle2 = this.picker.find('.slider-handle:last'); + this.handle2Stype = this.handle2[0].style; + + var handle = this.element.data('slider-handle')||options.handle; + switch(handle) { + case 'round': + this.handle1.addClass('round'); + this.handle2.addClass('round'); + break + case 'triangle': + this.handle1.addClass('triangle'); + this.handle2.addClass('triangle'); + break + } + + if (this.range) { + this.value[0] = Math.max(this.min, Math.min(this.max, this.value[0])); + this.value[1] = Math.max(this.min, Math.min(this.max, this.value[1])); + } else { + this.value = [ Math.max(this.min, Math.min(this.max, this.value))]; + this.handle2.addClass('hide'); + if (this.selection == 'after') { + this.value[1] = this.max; + } else { + this.value[1] = this.min; + } + } + this.diff = this.max - this.min; + this.percentage = [ + (this.value[0]-this.min)*100/this.diff, + (this.value[1]-this.min)*100/this.diff, + this.step*100/this.diff + ]; + + this.offset = this.picker.offset(); + this.size = this.picker[0][this.sizePos]; + + this.formater = options.formater; + + this.layout(); + + if (this.touchCapable) { + // Touch: Bind touch events: + this.picker.on({ + touchstart: $.proxy(this.mousedown, this) + }); + } else { + this.picker.on({ + mousedown: $.proxy(this.mousedown, this) + }); + } + + if (tooltip === 'show') { + this.picker.on({ + mouseenter: $.proxy(this.showTooltip, this), + mouseleave: $.proxy(this.hideTooltip, this) + }); + } else { + this.tooltip.addClass('hide'); + } + }; + + Slider.prototype = { + constructor: Slider, + + over: false, + inDrag: false, + + showTooltip: function(){ + this.tooltip.addClass('in'); + //var left = Math.round(this.percent*this.width); + //this.tooltip.css('left', left - this.tooltip.outerWidth()/2); + this.over = true; + }, + + hideTooltip: function(){ + if (this.inDrag === false) { + this.tooltip.removeClass('in'); + } + this.over = false; + }, + + layout: function(){ + this.handle1Stype[this.stylePos] = this.percentage[0]+'%'; + this.handle2Stype[this.stylePos] = this.percentage[1]+'%'; + if (this.orientation == 'vertical') { + this.selectionElStyle.top = Math.min(this.percentage[0], this.percentage[1]) +'%'; + this.selectionElStyle.height = Math.abs(this.percentage[0] - this.percentage[1]) +'%'; + } else { + this.selectionElStyle.left = Math.min(this.percentage[0], this.percentage[1]) +'%'; + this.selectionElStyle.width = Math.abs(this.percentage[0] - this.percentage[1]) +'%'; + } + if (this.range) { + this.tooltipInner.text( + this.formater(this.value[0]) + + ' : ' + + this.formater(this.value[1]) + ); + this.tooltip[0].style[this.stylePos] = this.size * (this.percentage[0] + (this.percentage[1] - this.percentage[0])/2)/100 - (this.orientation === 'vertical' ? this.tooltip.outerHeight()/2 : this.tooltip.outerWidth()/2) +'px'; + } else { + //console.log(this.value[0]); + this.tooltipInner.text( + this.formater(this.value[0]) + ); + this.tooltip[0].style[this.stylePos] = this.size * this.percentage[0]/100 - (this.orientation === 'vertical' ? this.tooltip.outerHeight()/2 : this.tooltip.outerWidth()/2) +'px'; + } + }, + + mousedown: function(ev) { + + // Touch: Get the original event: + if (this.touchCapable && ev.type === 'touchstart') { + ev = ev.originalEvent; + } + + this.offset = this.picker.offset(); + this.size = this.picker[0][this.sizePos]; + + var percentage = this.getPercentage(ev); + + if (this.range) { + var diff1 = Math.abs(this.percentage[0] - percentage); + var diff2 = Math.abs(this.percentage[1] - percentage); + this.dragged = (diff1 < diff2) ? 0 : 1; + } else { + this.dragged = 0; + } + + this.percentage[this.dragged] = percentage; + var val = this.calculateValue(); + + this.layout(); + + if (this.touchCapable) { + // Touch: Bind touch events: + $(document).on({ + touchmove: $.proxy(this.mousemove, this), + touchend: $.proxy(this.mouseup, this) + }); + } else { + $(document).on({ + mousemove: $.proxy(this.mousemove, this), + mouseup: $.proxy(this.mouseup, this) + }); + } + + this.inDrag = true; + this.element.trigger({ + type: 'slideStart', + value: val + }).trigger({ + type: 'slide', + value: val + }); + return false; + }, + + mousemove: function(ev) { + + // Touch: Get the original event: + if (this.touchCapable && ev.type === 'touchmove') { + ev = ev.originalEvent; + } + + var percentage = this.getPercentage(ev); + if (this.range) { + if (this.dragged === 0 && this.percentage[1] < percentage) { + this.percentage[0] = this.percentage[1]; + this.dragged = 1; + } else if (this.dragged === 1 && this.percentage[0] > percentage) { + this.percentage[1] = this.percentage[0]; + this.dragged = 0; + } + } + this.percentage[this.dragged] = percentage; + var val = this.calculateValue(); + this.layout(); + + this.element + .trigger({ + type: 'slide', + value: val + }) + .data('value', val) + .prop('value', val); + return false; + }, + + mouseup: function(ev) { + if (this.touchCapable) { + // Touch: Bind touch events: + $(document).off({ + touchmove: this.mousemove, + touchend: this.mouseup + }); + } else { + $(document).off({ + mousemove: this.mousemove, + mouseup: this.mouseup + }); + } + + this.inDrag = false; + if (this.over == false) { + this.hideTooltip(); + } + this.element; + var val = this.calculateValue(); + this.element + .trigger({ + type: 'slideStop', + value: val + }) + .data('value', val) + .prop('value', val); + return false; + }, + + calculateValue: function() { + var val; + if (this.range) { + val = [ + (this.min + Math.round((this.diff * this.percentage[0]/100)/this.step)*this.step), + (this.min + Math.round((this.diff * this.percentage[1]/100)/this.step)*this.step) + ]; + this.value = val; + } else { + val = (this.min + Math.round((this.diff * this.percentage[0]/100)/this.step)*this.step); + this.value = [val, this.value[1]]; + } + return val; + }, + + getPercentage: function(ev) { + if (this.touchCapable) { + ev = ev.touches[0]; + } + var percentage = (ev[this.mousePos] - this.offset[this.stylePos])*100/this.size; + percentage = Math.round(percentage/this.percentage[2])*this.percentage[2]; + return Math.max(0, Math.min(100, percentage)); + }, + + getValue: function() { + if (this.range) { + return this.value; + } + return this.value[0]; + }, + + setValue: function(val) { + this.value = val; + + if (this.range) { + this.value[0] = Math.max(this.min, Math.min(this.max, this.value[0])); + this.value[1] = Math.max(this.min, Math.min(this.max, this.value[1])); + } else { + this.value = [ Math.max(this.min, Math.min(this.max, this.value))]; + this.handle2.addClass('hide'); + if (this.selection == 'after') { + this.value[1] = this.max; + } else { + this.value[1] = this.min; + } + } + this.diff = this.max - this.min; + this.percentage = [ + (this.value[0]-this.min)*100/this.diff, + (this.value[1]-this.min)*100/this.diff, + this.step*100/this.diff + ]; + this.layout(); + } + }; + + $.fn.slider = function ( option, val ) { + return this.each(function () { + var $this = $(this), + data = $this.data('slider'), + options = typeof option === 'object' && option; + if (!data) { + $this.data('slider', (data = new Slider(this, $.extend({}, $.fn.slider.defaults,options)))); + } + if (typeof option == 'string') { + data[option](val); + } + }) + }; + + $.fn.slider.defaults = { + min: 0, + max: 100, + step: 1, + orientation: 'horizontal', + value: 100, + selection: 'before', + tooltip: 'show', + handle: 'round', + formater: function(value) { + return value; + } + }; + + $.fn.slider.Constructor = Slider; + +}( window.jQuery ); \ No newline at end of file diff --git a/cat-home/src/main/webapp/js/rickshaw.js b/cat-home/src/main/webapp/js/rickshaw.js new file mode 100644 index 0000000000000000000000000000000000000000..3c74a86034f5c09991d70f801beba222631d2bbf --- /dev/null +++ b/cat-home/src/main/webapp/js/rickshaw.js @@ -0,0 +1,3388 @@ +var Rickshaw = { + + namespace: function(namespace, obj) { + + var parts = namespace.split('.'); + + var parent = Rickshaw; + + for(var i = 1, length = parts.length; i < length; i++) { + var currentPart = parts[i]; + parent[currentPart] = parent[currentPart] || {}; + parent = parent[currentPart]; + } + return parent; + }, + + keys: function(obj) { + var keys = []; + for (var key in obj) keys.push(key); + return keys; + }, + + extend: function(destination, source) { + + for (var property in source) { + destination[property] = source[property]; + } + return destination; + }, + + clone: function(obj) { + return JSON.parse(JSON.stringify(obj)); + } +}; + +if (typeof module !== 'undefined' && module.exports) { + var d3 = require('d3'); + module.exports = Rickshaw; +} + +/* Adapted from https://github.com/Jakobo/PTClass */ + +/* +Copyright (c) 2005-2010 Sam Stephenson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +/* Based on Alex Arnell's inheritance implementation. */ +/** section: Language + * class Class + * + * Manages Prototype's class-based OOP system. + * + * Refer to Prototype's web site for a [tutorial on classes and + * inheritance](http://prototypejs.org/learn/class-inheritance). +**/ +(function(globalContext) { +/* ------------------------------------ */ +/* Import from object.js */ +/* ------------------------------------ */ +var _toString = Object.prototype.toString, + NULL_TYPE = 'Null', + UNDEFINED_TYPE = 'Undefined', + BOOLEAN_TYPE = 'Boolean', + NUMBER_TYPE = 'Number', + STRING_TYPE = 'String', + OBJECT_TYPE = 'Object', + FUNCTION_CLASS = '[object Function]'; +function isFunction(object) { + return _toString.call(object) === FUNCTION_CLASS; +} +function extend(destination, source) { + for (var property in source) if (source.hasOwnProperty(property)) // modify protect primitive slaughter + destination[property] = source[property]; + return destination; +} +function keys(object) { + if (Type(object) !== OBJECT_TYPE) { throw new TypeError(); } + var results = []; + for (var property in object) { + if (object.hasOwnProperty(property)) { + results.push(property); + } + } + return results; +} +function Type(o) { + switch(o) { + case null: return NULL_TYPE; + case (void 0): return UNDEFINED_TYPE; + } + var type = typeof o; + switch(type) { + case 'boolean': return BOOLEAN_TYPE; + case 'number': return NUMBER_TYPE; + case 'string': return STRING_TYPE; + } + return OBJECT_TYPE; +} +function isUndefined(object) { + return typeof object === "undefined"; +} +/* ------------------------------------ */ +/* Import from Function.js */ +/* ------------------------------------ */ +var slice = Array.prototype.slice; +function argumentNames(fn) { + var names = fn.toString().match(/^[\s\(]*function[^(]*\(([^)]*)\)/)[1] + .replace(/\/\/.*?[\r\n]|\/\*(?:.|[\r\n])*?\*\//g, '') + .replace(/\s+/g, '').split(','); + return names.length == 1 && !names[0] ? [] : names; +} +function wrap(fn, wrapper) { + var __method = fn; + return function() { + var a = update([bind(__method, this)], arguments); + return wrapper.apply(this, a); + } +} +function update(array, args) { + var arrayLength = array.length, length = args.length; + while (length--) array[arrayLength + length] = args[length]; + return array; +} +function merge(array, args) { + array = slice.call(array, 0); + return update(array, args); +} +function bind(fn, context) { + if (arguments.length < 2 && isUndefined(arguments[0])) return this; + var __method = fn, args = slice.call(arguments, 2); + return function() { + var a = merge(args, arguments); + return __method.apply(context, a); + } +} + +/* ------------------------------------ */ +/* Import from Prototype.js */ +/* ------------------------------------ */ +var emptyFunction = function(){}; + +var Class = (function() { + + // Some versions of JScript fail to enumerate over properties, names of which + // correspond to non-enumerable properties in the prototype chain + var IS_DONTENUM_BUGGY = (function(){ + for (var p in { toString: 1 }) { + // check actual property name, so that it works with augmented Object.prototype + if (p === 'toString') return false; + } + return true; + })(); + + function subclass() {}; + function create() { + var parent = null, properties = [].slice.apply(arguments); + if (isFunction(properties[0])) + parent = properties.shift(); + + function klass() { + this.initialize.apply(this, arguments); + } + + extend(klass, Class.Methods); + klass.superclass = parent; + klass.subclasses = []; + + if (parent) { + subclass.prototype = parent.prototype; + klass.prototype = new subclass; + try { parent.subclasses.push(klass) } catch(e) {} + } + + for (var i = 0, length = properties.length; i < length; i++) + klass.addMethods(properties[i]); + + if (!klass.prototype.initialize) + klass.prototype.initialize = emptyFunction; + + klass.prototype.constructor = klass; + return klass; + } + + function addMethods(source) { + var ancestor = this.superclass && this.superclass.prototype, + properties = keys(source); + + // IE6 doesn't enumerate `toString` and `valueOf` (among other built-in `Object.prototype`) properties, + // Force copy if they're not Object.prototype ones. + // Do not copy other Object.prototype.* for performance reasons + if (IS_DONTENUM_BUGGY) { + if (source.toString != Object.prototype.toString) + properties.push("toString"); + if (source.valueOf != Object.prototype.valueOf) + properties.push("valueOf"); + } + + for (var i = 0, length = properties.length; i < length; i++) { + var property = properties[i], value = source[property]; + if (ancestor && isFunction(value) && + argumentNames(value)[0] == "$super") { + var method = value; + value = wrap((function(m) { + return function() { return ancestor[m].apply(this, arguments); }; + })(property), method); + + value.valueOf = bind(method.valueOf, method); + value.toString = bind(method.toString, method); + } + this.prototype[property] = value; + } + + return this; + } + + return { + create: create, + Methods: { + addMethods: addMethods + } + }; +})(); + +if (globalContext.exports) { + globalContext.exports.Class = Class; +} +else { + globalContext.Class = Class; +} +})(Rickshaw); +Rickshaw.namespace('Rickshaw.Compat.ClassList'); + +Rickshaw.Compat.ClassList = function() { + + /* adapted from http://purl.eligrey.com/github/classList.js/blob/master/classList.js */ + + if (typeof document !== "undefined" && !("classList" in document.createElement("a"))) { + + (function (view) { + + "use strict"; + + var + classListProp = "classList" + , protoProp = "prototype" + , elemCtrProto = (view.HTMLElement || view.Element)[protoProp] + , objCtr = Object + , strTrim = String[protoProp].trim || function () { + return this.replace(/^\s+|\s+$/g, ""); + } + , arrIndexOf = Array[protoProp].indexOf || function (item) { + var + i = 0 + , len = this.length + ; + for (; i < len; i++) { + if (i in this && this[i] === item) { + return i; + } + } + return -1; + } + // Vendors: please allow content code to instantiate DOMExceptions + , DOMEx = function (type, message) { + this.name = type; + this.code = DOMException[type]; + this.message = message; + } + , checkTokenAndGetIndex = function (classList, token) { + if (token === "") { + throw new DOMEx( + "SYNTAX_ERR" + , "An invalid or illegal string was specified" + ); + } + if (/\s/.test(token)) { + throw new DOMEx( + "INVALID_CHARACTER_ERR" + , "String contains an invalid character" + ); + } + return arrIndexOf.call(classList, token); + } + , ClassList = function (elem) { + var + trimmedClasses = strTrim.call(elem.className) + , classes = trimmedClasses ? trimmedClasses.split(/\s+/) : [] + , i = 0 + , len = classes.length + ; + for (; i < len; i++) { + this.push(classes[i]); + } + this._updateClassName = function () { + elem.className = this.toString(); + }; + } + , classListProto = ClassList[protoProp] = [] + , classListGetter = function () { + return new ClassList(this); + } + ; + // Most DOMException implementations don't allow calling DOMException's toString() + // on non-DOMExceptions. Error's toString() is sufficient here. + DOMEx[protoProp] = Error[protoProp]; + classListProto.item = function (i) { + return this[i] || null; + }; + classListProto.contains = function (token) { + token += ""; + return checkTokenAndGetIndex(this, token) !== -1; + }; + classListProto.add = function (token) { + token += ""; + if (checkTokenAndGetIndex(this, token) === -1) { + this.push(token); + this._updateClassName(); + } + }; + classListProto.remove = function (token) { + token += ""; + var index = checkTokenAndGetIndex(this, token); + if (index !== -1) { + this.splice(index, 1); + this._updateClassName(); + } + }; + classListProto.toggle = function (token) { + token += ""; + if (checkTokenAndGetIndex(this, token) === -1) { + this.add(token); + } else { + this.remove(token); + } + }; + classListProto.toString = function () { + return this.join(" "); + }; + + if (objCtr.defineProperty) { + var classListPropDesc = { + get: classListGetter + , enumerable: true + , configurable: true + }; + try { + objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc); + } catch (ex) { // IE 8 doesn't support enumerable:true + if (ex.number === -0x7FF5EC54) { + classListPropDesc.enumerable = false; + objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc); + } + } + } else if (objCtr[protoProp].__defineGetter__) { + elemCtrProto.__defineGetter__(classListProp, classListGetter); + } + + }(window)); + + } +}; + +if ( (typeof RICKSHAW_NO_COMPAT !== "undefined" && !RICKSHAW_NO_COMPAT) || typeof RICKSHAW_NO_COMPAT === "undefined") { + new Rickshaw.Compat.ClassList(); +} +Rickshaw.namespace('Rickshaw.Graph'); + +Rickshaw.Graph = function(args) { + + if (!args.element) throw "Rickshaw.Graph needs a reference to an element"; + + this.element = args.element; + this.series = args.series; + + this.defaults = { + interpolation: 'cardinal', + offset: 'zero', + min: undefined, + max: undefined, + preserve: false + }; + + Rickshaw.keys(this.defaults).forEach( function(k) { + this[k] = args[k] || this.defaults[k]; + }, this ); + + this.window = {}; + + this.updateCallbacks = []; + + var self = this; + + this.initialize = function(args) { + + this.validateSeries(args.series); + + this.series.active = function() { return self.series.filter( function(s) { return !s.disabled } ) }; + + this.setSize({ width: args.width, height: args.height }); + + this.element.classList.add('rickshaw_graph'); + this.vis = d3.select(this.element) + .append("svg:svg") + .attr('width', this.width) + .attr('height', this.height); + + for (var name in Rickshaw.Graph.Renderer) { + if (!name || !Rickshaw.Graph.Renderer.hasOwnProperty(name)) continue; + var r = Rickshaw.Graph.Renderer[name]; + if (!r || !r.prototype || !r.prototype.render) continue; + self.registerRenderer(new r( { graph: self } )); + } + + this.setRenderer(args.renderer || 'stack', args); + this.discoverRange(); + }; + + this.validateSeries = function(series) { + + if (!Array.isArray(series) && !(series instanceof Rickshaw.Series)) { + var seriesSignature = Object.prototype.toString.apply(series); + throw "series is not an array: " + seriesSignature; + } + + var pointsCount; + + series.forEach( function(s) { + + if (!(s instanceof Object)) { + throw "series element is not an object: " + s; + } + if (!(s.data)) { + throw "series has no data: " + JSON.stringify(s); + } + if (!Array.isArray(s.data)) { + throw "series data is not an array: " + JSON.stringify(s.data); + } + + var x = s.data[0].x; + var y = s.data[0].y; + + if (typeof x != 'number' || ( typeof y != 'number' && y !== null ) ) { + throw "x and y properties of points should be numbers instead of " + + (typeof x) + " and " + (typeof y); + } + + if (s.data.length >= 3) { + // probe to sanity check sort order + if (s.data[2].x < s.data[1].x || s.data[1].x < s.data[0].x || s.data[s.data.length - 1].x < s.data[0].x) { + throw "series data needs to be sorted on x values for series name: " + s.name; + } + } + + }, this ); + }; + + this.dataDomain = function() { + + var data = this.series.map( function(s) { return s.data } ); + + var min = d3.min( data.map( function(d) { return d[0].x } ) ); + var max = d3.max( data.map( function(d) { return d[d.length - 1].x } ) ); + + return [min, max]; + }; + + this.discoverRange = function() { + + var domain = this.renderer.domain(); + + this.x = d3.scale.linear().domain(domain.x).range([0, this.width]); + + this.y = d3.scale.linear().domain(domain.y).range([this.height, 0]); + + this.y.magnitude = d3.scale.linear() + .domain([domain.y[0] - domain.y[0], domain.y[1] - domain.y[0]]) + .range([0, this.height]); + }; + + this.render = function() { + + var stackedData = this.stackData(); + this.discoverRange(); + + this.renderer.render(); + + this.updateCallbacks.forEach( function(callback) { + callback(); + } ); + }; + + this.update = this.render; + + this.stackData = function() { + + var data = this.series.active() + .map( function(d) { return d.data } ) + .map( function(d) { return d.filter( function(d) { return this._slice(d) }, this ) }, this); + + var preserve = this.preserve; + if (!preserve) { + this.series.forEach( function(series) { + if (series.scale) { + // data must be preserved when a scale is used + preserve = true; + } + } ); + } + + data = preserve ? Rickshaw.clone(data) : data; + + this.series.active().forEach( function(series, index) { + if (series.scale) { + // apply scale to each series + var seriesData = data[index]; + if(seriesData) { + seriesData.forEach( function(d) { + d.y = series.scale(d.y); + } ); + } + } + } ); + + this.stackData.hooks.data.forEach( function(entry) { + data = entry.f.apply(self, [data]); + } ); + + var stackedData; + + if (!this.renderer.unstack) { + + this._validateStackable(); + + var layout = d3.layout.stack(); + layout.offset( self.offset ); + stackedData = layout(data); + } + + stackedData = stackedData || data; + + this.stackData.hooks.after.forEach( function(entry) { + stackedData = entry.f.apply(self, [data]); + } ); + + + var i = 0; + this.series.forEach( function(series) { + if (series.disabled) return; + series.stack = stackedData[i++]; + } ); + + this.stackedData = stackedData; + return stackedData; + }; + + this._validateStackable = function() { + + var series = this.series; + var pointsCount; + + series.forEach( function(s) { + + pointsCount = pointsCount || s.data.length; + + if (pointsCount && s.data.length != pointsCount) { + throw "stacked series cannot have differing numbers of points: " + + pointsCount + " vs " + s.data.length + "; see Rickshaw.Series.fill()"; + } + + }, this ); + }; + + this.stackData.hooks = { data: [], after: [] }; + + this._slice = function(d) { + + if (this.window.xMin || this.window.xMax) { + + var isInRange = true; + + if (this.window.xMin && d.x < this.window.xMin) isInRange = false; + if (this.window.xMax && d.x > this.window.xMax) isInRange = false; + + return isInRange; + } + + return true; + }; + + this.onUpdate = function(callback) { + this.updateCallbacks.push(callback); + }; + + this.registerRenderer = function(renderer) { + this._renderers = this._renderers || {}; + this._renderers[renderer.name] = renderer; + }; + + this.configure = function(args) { + + if (args.width || args.height) { + this.setSize(args); + } + + Rickshaw.keys(this.defaults).forEach( function(k) { + this[k] = k in args ? args[k] + : k in this ? this[k] + : this.defaults[k]; + }, this ); + + this.setRenderer(args.renderer || this.renderer.name, args); + }; + + this.setRenderer = function(r, args) { + if (typeof r == 'function') { + this.renderer = new r( { graph: self } ); + this.registerRenderer(this.renderer); + } else { + if (!this._renderers[r]) { + throw "couldn't find renderer " + r; + } + this.renderer = this._renderers[r]; + } + + if (typeof args == 'object') { + this.renderer.configure(args); + } + }; + + this.setSize = function(args) { + + args = args || {}; + + if (typeof window !== undefined) { + var style = window.getComputedStyle(this.element, null); + var elementWidth = parseInt(style.getPropertyValue('width'), 10); + var elementHeight = parseInt(style.getPropertyValue('height'), 10); + } + + this.width = args.width || elementWidth || 400; + this.height = args.height || elementHeight || 250; + + this.vis && this.vis + .attr('width', this.width) + .attr('height', this.height); + }; + + this.initialize(args); +}; +Rickshaw.namespace('Rickshaw.Fixtures.Color'); + +Rickshaw.Fixtures.Color = function() { + + this.schemes = {}; + + this.schemes.spectrum14 = [ + '#ecb796', + '#dc8f70', + '#b2a470', + '#92875a', + '#716c49', + '#d2ed82', + '#bbe468', + '#a1d05d', + '#e7cbe6', + '#d8aad6', + '#a888c2', + '#9dc2d3', + '#649eb9', + '#387aa3' + ].reverse(); + + this.schemes.spectrum2000 = [ + '#57306f', + '#514c76', + '#646583', + '#738394', + '#6b9c7d', + '#84b665', + '#a7ca50', + '#bfe746', + '#e2f528', + '#fff726', + '#ecdd00', + '#d4b11d', + '#de8800', + '#de4800', + '#c91515', + '#9a0000', + '#7b0429', + '#580839', + '#31082b' + ]; + + this.schemes.spectrum2001 = [ + '#2f243f', + '#3c2c55', + '#4a3768', + '#565270', + '#6b6b7c', + '#72957f', + '#86ad6e', + '#a1bc5e', + '#b8d954', + '#d3e04e', + '#ccad2a', + '#cc8412', + '#c1521d', + '#ad3821', + '#8a1010', + '#681717', + '#531e1e', + '#3d1818', + '#320a1b' + ]; + + this.schemes.classic9 = [ + '#423d4f', + '#4a6860', + '#848f39', + '#a2b73c', + '#ddcb53', + '#c5a32f', + '#7d5836', + '#963b20', + '#7c2626', + '#491d37', + '#2f254a' + ].reverse(); + + this.schemes.httpStatus = { + 503: '#ea5029', + 502: '#d23f14', + 500: '#bf3613', + 410: '#efacea', + 409: '#e291dc', + 403: '#f457e8', + 408: '#e121d2', + 401: '#b92dae', + 405: '#f47ceb', + 404: '#a82a9f', + 400: '#b263c6', + 301: '#6fa024', + 302: '#87c32b', + 307: '#a0d84c', + 304: '#28b55c', + 200: '#1a4f74', + 206: '#27839f', + 201: '#52adc9', + 202: '#7c979f', + 203: '#a5b8bd', + 204: '#c1cdd1' + }; + + this.schemes.colorwheel = [ + '#b5b6a9', + '#858772', + '#785f43', + '#96557e', + '#4682b4', + '#65b9ac', + '#73c03a', + '#cb513a' + ].reverse(); + + this.schemes.cool = [ + '#5e9d2f', + '#73c03a', + '#4682b4', + '#7bc3b8', + '#a9884e', + '#c1b266', + '#a47493', + '#c09fb5' + ]; + + this.schemes.munin = [ + '#00cc00', + '#0066b3', + '#ff8000', + '#ffcc00', + '#330099', + '#990099', + '#ccff00', + '#ff0000', + '#808080', + '#008f00', + '#00487d', + '#b35a00', + '#b38f00', + '#6b006b', + '#8fb300', + '#b30000', + '#bebebe', + '#80ff80', + '#80c9ff', + '#ffc080', + '#ffe680', + '#aa80ff', + '#ee00cc', + '#ff8080', + '#666600', + '#ffbfff', + '#00ffcc', + '#cc6699', + '#999900' + ]; +}; +Rickshaw.namespace('Rickshaw.Fixtures.RandomData'); + +Rickshaw.Fixtures.RandomData = function(timeInterval) { + + var addData; + timeInterval = timeInterval || 1; + + var lastRandomValue = 200; + + var timeBase = Math.floor(new Date().getTime() / 1000); + + this.addData = function(data) { + + var randomValue = Math.random() * 100 + 15 + lastRandomValue; + var index = data[0].length; + + var counter = 1; + + data.forEach( function(series) { + var randomVariance = Math.random() * 20; + var v = randomValue / 25 + counter++ + + (Math.cos((index * counter * 11) / 960) + 2) * 15 + + (Math.cos(index / 7) + 2) * 7 + + (Math.cos(index / 17) + 2) * 1; + + series.push( { x: (index * timeInterval) + timeBase, y: v + randomVariance } ); + } ); + + lastRandomValue = randomValue * 0.85; + }; + + this.removeData = function(data) { + data.forEach( function(series) { + series.shift(); + } ); + timeBase += timeInterval; + }; +}; + +Rickshaw.namespace('Rickshaw.Fixtures.Time'); + +Rickshaw.Fixtures.Time = function() { + + var tzOffset = new Date().getTimezoneOffset() * 60; + + var self = this; + + this.months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + + this.units = [ + { + name: 'decade', + seconds: 86400 * 365.25 * 10, + formatter: function(d) { return (parseInt(d.getUTCFullYear() / 10, 10) * 10) } + }, { + name: 'year', + seconds: 86400 * 365.25, + formatter: function(d) { return d.getUTCFullYear() } + }, { + name: 'month', + seconds: 86400 * 30.5, + formatter: function(d) { return self.months[d.getUTCMonth()] } + }, { + name: 'week', + seconds: 86400 * 7, + formatter: function(d) { return self.formatDate(d) } + }, { + name: 'day', + seconds: 86400, + formatter: function(d) { return d.getUTCDate() } + }, { + name: '6 hour', + seconds: 3600 * 6, + formatter: function(d) { return self.formatTime(d) } + }, { + name: 'hour', + seconds: 3600, + formatter: function(d) { return self.formatTime(d) } + }, { + name: '15 minute', + seconds: 60 * 15, + formatter: function(d) { return self.formatTime(d) } + }, { + name: 'minute', + seconds: 60, + formatter: function(d) { return d.getUTCMinutes() } + }, { + name: '15 second', + seconds: 15, + formatter: function(d) { return d.getUTCSeconds() + 's' } + }, { + name: 'second', + seconds: 1, + formatter: function(d) { return d.getUTCSeconds() + 's' } + } + ]; + + this.unit = function(unitName) { + return this.units.filter( function(unit) { return unitName == unit.name } ).shift(); + }; + + this.formatDate = function(d) { + return d3.time.format('%b %e')(d); + }; + + this.formatTime = function(d) { + return d.toUTCString().match(/(\d+:\d+):/)[1]; + }; + + this.ceil = function(time, unit) { + + var nearFuture; + var rounded; + + if (unit.name == 'month') { + + nearFuture = new Date((time + unit.seconds - 1) * 1000); + + rounded = new Date(0); + rounded.setUTCFullYear(nearFuture.getUTCFullYear()); + rounded.setUTCMonth(nearFuture.getUTCMonth()); + rounded.setUTCDate(1); + rounded.setUTCHours(0); + rounded.setUTCMinutes(0); + rounded.setUTCSeconds(0); + rounded.setUTCMilliseconds(0); + + return rounded.getTime() / 1000; + } + + if (unit.name == 'year') { + + nearFuture = new Date((time + unit.seconds - 1) * 1000); + + rounded = new Date(0); + rounded.setUTCFullYear(nearFuture.getUTCFullYear()); + rounded.setUTCMonth(0); + rounded.setUTCDate(1); + rounded.setUTCHours(0); + rounded.setUTCMinutes(0); + rounded.setUTCSeconds(0); + rounded.setUTCMilliseconds(0); + + return rounded.getTime() / 1000; + } + + return Math.ceil(time / unit.seconds) * unit.seconds; + }; +}; +Rickshaw.namespace('Rickshaw.Fixtures.Time.Local'); + +Rickshaw.Fixtures.Time.Local = function() { + + var tzOffset = new Date().getTimezoneOffset() * 60; + + var self = this; + + this.months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + + this.units = [ + { + name: 'decade', + seconds: 86400 * 365.25 * 10, + formatter: function(d) { return (parseInt(d.getFullYear() / 10, 10) * 10) } + }, { + name: 'year', + seconds: 86400 * 365.25, + formatter: function(d) { return d.getFullYear() } + }, { + name: 'month', + seconds: 86400 * 30.5, + formatter: function(d) { return self.months[d.getMonth()] } + }, { + name: 'week', + seconds: 86400 * 7, + formatter: function(d) { return self.formatDate(d) } + }, { + name: 'day', + seconds: 86400, + formatter: function(d) { return d.getDate() } + }, { + name: '6 hour', + seconds: 3600 * 6, + formatter: function(d) { return self.formatTime(d) } + }, { + name: 'hour', + seconds: 3600, + formatter: function(d) { return self.formatTime(d) } + }, { + name: '15 minute', + seconds: 60 * 15, + formatter: function(d) { return self.formatTime(d) } + }, { + name: 'minute', + seconds: 60, + formatter: function(d) { return d.getMinutes() } + }, { + name: '15 second', + seconds: 15, + formatter: function(d) { return d.getSeconds() + 's' } + }, { + name: 'second', + seconds: 1, + formatter: function(d) { return d.getSeconds() + 's' } + } + ]; + + this.unit = function(unitName) { + return this.units.filter( function(unit) { return unitName == unit.name } ).shift(); + }; + + this.formatDate = function(d) { + return d3.time.format('%b %e')(d); + }; + + this.formatTime = function(d) { + return d.toString().match(/(\d+:\d+):/)[1]; + }; + + this.ceil = function(time, unit) { + + var nearFuture; + var rounded; + + if (unit.name == 'day') { + + nearFuture = new Date((time + unit.seconds - 1) * 1000); + + rounded = new Date(0); + rounded.setMilliseconds(0); + rounded.setSeconds(0); + rounded.setMinutes(0); + rounded.setHours(0); + rounded.setDate(nearFuture.getDate()); + rounded.setMonth(nearFuture.getMonth()); + rounded.setFullYear(nearFuture.getFullYear()); + + return rounded.getTime() / 1000; + } + + if (unit.name == 'month') { + + nearFuture = new Date((time + unit.seconds - 1) * 1000); + + rounded = new Date(0); + rounded.setMilliseconds(0); + rounded.setSeconds(0); + rounded.setMinutes(0); + rounded.setHours(0); + rounded.setDate(1); + rounded.setMonth(nearFuture.getMonth()); + rounded.setFullYear(nearFuture.getFullYear()); + + return rounded.getTime() / 1000; + } + + if (unit.name == 'year') { + + nearFuture = new Date((time + unit.seconds - 1) * 1000); + + rounded = new Date(0); + rounded.setFullYear(nearFuture.getFullYear()); + rounded.setMilliseconds(0); + rounded.setSeconds(0); + rounded.setMinutes(0); + rounded.setHours(0); + rounded.setDate(1); + rounded.setMonth(0); + + return rounded.getTime() / 1000; + } + + return Math.ceil(time / unit.seconds) * unit.seconds; + }; +}; +Rickshaw.namespace('Rickshaw.Fixtures.Number'); + +Rickshaw.Fixtures.Number.formatKMBT = function(y) { + var abs_y = Math.abs(y); + if (abs_y >= 1000000000000) { return y / 1000000000000 + "T" } + else if (abs_y >= 1000000000) { return y / 1000000000 + "B" } + else if (abs_y >= 1000000) { return y / 1000000 + "M" } + else if (abs_y >= 1000) { return y / 1000 + "K" } + else if (abs_y < 1 && y > 0) { return y.toFixed(2) } + else if (abs_y === 0) { return '' } + else { return y } +}; + +Rickshaw.Fixtures.Number.formatBase1024KMGTP = function(y) { + var abs_y = Math.abs(y); + if (abs_y >= 1125899906842624) { return y / 1125899906842624 + "P" } + else if (abs_y >= 1099511627776){ return y / 1099511627776 + "T" } + else if (abs_y >= 1073741824) { return y / 1073741824 + "G" } + else if (abs_y >= 1048576) { return y / 1048576 + "M" } + else if (abs_y >= 1024) { return y / 1024 + "K" } + else if (abs_y < 1 && y > 0) { return y.toFixed(2) } + else if (abs_y === 0) { return '' } + else { return y } +}; +Rickshaw.namespace("Rickshaw.Color.Palette"); + +Rickshaw.Color.Palette = function(args) { + + var color = new Rickshaw.Fixtures.Color(); + + args = args || {}; + this.schemes = {}; + + this.scheme = color.schemes[args.scheme] || args.scheme || color.schemes.colorwheel; + this.runningIndex = 0; + this.generatorIndex = 0; + + if (args.interpolatedStopCount) { + var schemeCount = this.scheme.length - 1; + var i, j, scheme = []; + for (i = 0; i < schemeCount; i++) { + scheme.push(this.scheme[i]); + var generator = d3.interpolateHsl(this.scheme[i], this.scheme[i + 1]); + for (j = 1; j < args.interpolatedStopCount; j++) { + scheme.push(generator((1 / args.interpolatedStopCount) * j)); + } + } + scheme.push(this.scheme[this.scheme.length - 1]); + this.scheme = scheme; + } + this.rotateCount = this.scheme.length; + + this.color = function(key) { + return this.scheme[key] || this.scheme[this.runningIndex++] || this.interpolateColor() || '#808080'; + }; + + this.interpolateColor = function() { + if (!Array.isArray(this.scheme)) return; + var color; + if (this.generatorIndex == this.rotateCount * 2 - 1) { + color = d3.interpolateHsl(this.scheme[this.generatorIndex], this.scheme[0])(0.5); + this.generatorIndex = 0; + this.rotateCount *= 2; + } else { + color = d3.interpolateHsl(this.scheme[this.generatorIndex], this.scheme[this.generatorIndex + 1])(0.5); + this.generatorIndex++; + } + this.scheme.push(color); + return color; + }; + +}; +Rickshaw.namespace('Rickshaw.Graph.Ajax'); + +Rickshaw.Graph.Ajax = Rickshaw.Class.create( { + + initialize: function(args) { + + this.dataURL = args.dataURL; + + this.onData = args.onData || function(d) { return d }; + this.onComplete = args.onComplete || function() {}; + this.onError = args.onError || function() {}; + + this.args = args; // pass through to Rickshaw.Graph + + this.request(); + }, + + request: function() { + + $.ajax( { + url: this.dataURL, + dataType: 'json', + success: this.success.bind(this), + error: this.error.bind(this) + } ); + }, + + error: function() { + + console.log("error loading dataURL: " + this.dataURL); + this.onError(this); + }, + + success: function(data, status) { + + data = this.onData(data); + this.args.series = this._splice({ data: data, series: this.args.series }); + + this.graph = this.graph || new Rickshaw.Graph(this.args); + this.graph.render(); + + this.onComplete(this); + }, + + _splice: function(args) { + + var data = args.data; + var series = args.series; + + if (!args.series) return data; + + series.forEach( function(s) { + + var seriesKey = s.key || s.name; + if (!seriesKey) throw "series needs a key or a name"; + + data.forEach( function(d) { + + var dataKey = d.key || d.name; + if (!dataKey) throw "data needs a key or a name"; + + if (seriesKey == dataKey) { + var properties = ['color', 'name', 'data']; + properties.forEach( function(p) { + if (d[p]) s[p] = d[p]; + } ); + } + } ); + } ); + + return series; + } +} ); + +Rickshaw.namespace('Rickshaw.Graph.Annotate'); + +Rickshaw.Graph.Annotate = function(args) { + + var graph = this.graph = args.graph; + this.elements = { timeline: args.element }; + + var self = this; + + this.data = {}; + + this.elements.timeline.classList.add('rickshaw_annotation_timeline'); + + this.add = function(time, content, end_time) { + self.data[time] = self.data[time] || {'boxes': []}; + self.data[time].boxes.push({content: content, end: end_time}); + }; + + this.update = function() { + + Rickshaw.keys(self.data).forEach( function(time) { + + var annotation = self.data[time]; + var left = self.graph.x(time); + + if (left < 0 || left > self.graph.x.range()[1]) { + if (annotation.element) { + annotation.line.classList.add('offscreen'); + annotation.element.style.display = 'none'; + } + + annotation.boxes.forEach( function(box) { + if ( box.rangeElement ) box.rangeElement.classList.add('offscreen'); + }); + + return; + } + + if (!annotation.element) { + var element = annotation.element = document.createElement('div'); + element.classList.add('annotation'); + this.elements.timeline.appendChild(element); + element.addEventListener('click', function(e) { + element.classList.toggle('active'); + annotation.line.classList.toggle('active'); + annotation.boxes.forEach( function(box) { + if ( box.rangeElement ) box.rangeElement.classList.toggle('active'); + }); + }, false); + + } + + annotation.element.style.left = left + 'px'; + annotation.element.style.display = 'block'; + + annotation.boxes.forEach( function(box) { + + + var element = box.element; + + if (!element) { + element = box.element = document.createElement('div'); + element.classList.add('content'); + element.innerHTML = box.content; + annotation.element.appendChild(element); + + annotation.line = document.createElement('div'); + annotation.line.classList.add('annotation_line'); + self.graph.element.appendChild(annotation.line); + + if ( box.end ) { + box.rangeElement = document.createElement('div'); + box.rangeElement.classList.add('annotation_range'); + self.graph.element.appendChild(box.rangeElement); + } + + } + + if ( box.end ) { + + var annotationRangeStart = left; + var annotationRangeEnd = Math.min( self.graph.x(box.end), self.graph.x.range()[1] ); + + // annotation makes more sense at end + if ( annotationRangeStart > annotationRangeEnd ) { + annotationRangeEnd = left; + annotationRangeStart = Math.max( self.graph.x(box.end), self.graph.x.range()[0] ); + } + + var annotationRangeWidth = annotationRangeEnd - annotationRangeStart; + + box.rangeElement.style.left = annotationRangeStart + 'px'; + box.rangeElement.style.width = annotationRangeWidth + 'px'; + + box.rangeElement.classList.remove('offscreen'); + } + + annotation.line.classList.remove('offscreen'); + annotation.line.style.left = left + 'px'; + } ); + }, this ); + }; + + this.graph.onUpdate( function() { self.update() } ); +}; +Rickshaw.namespace('Rickshaw.Graph.Axis.Time'); + +Rickshaw.Graph.Axis.Time = function(args) { + + var self = this; + + this.graph = args.graph; + this.elements = []; + this.ticksTreatment = args.ticksTreatment || 'plain'; + this.fixedTimeUnit = args.timeUnit; + + var time = args.timeFixture || new Rickshaw.Fixtures.Time(); + + this.appropriateTimeUnit = function() { + + var unit; + var units = time.units; + + var domain = this.graph.x.domain(); + var rangeSeconds = domain[1] - domain[0]; + + units.forEach( function(u) { + if (Math.floor(rangeSeconds / u.seconds) >= 2) { + unit = unit || u; + } + } ); + + return (unit || time.units[time.units.length - 1]); + }; + + this.tickOffsets = function() { + + var domain = this.graph.x.domain(); + + var unit = this.fixedTimeUnit || this.appropriateTimeUnit(); + var count = Math.ceil((domain[1] - domain[0]) / unit.seconds); + + var runningTick = domain[0]; + + var offsets = []; + + for (var i = 0; i < count; i++) { + + var tickValue = time.ceil(runningTick, unit); + runningTick = tickValue + unit.seconds / 2; + + offsets.push( { value: tickValue, unit: unit } ); + } + + return offsets; + }; + + this.render = function() { + + this.elements.forEach( function(e) { + e.parentNode.removeChild(e); + } ); + + this.elements = []; + + var offsets = this.tickOffsets(); + + offsets.forEach( function(o) { + + if (self.graph.x(o.value) > self.graph.x.range()[1]) return; + + var element = document.createElement('div'); + element.style.left = self.graph.x(o.value) + 'px'; + element.classList.add('x_tick'); + element.classList.add(self.ticksTreatment); + + var title = document.createElement('div'); + title.classList.add('title'); + title.innerHTML = o.unit.formatter(new Date(o.value * 1000)); + element.appendChild(title); + + self.graph.element.appendChild(element); + self.elements.push(element); + + } ); + }; + + this.graph.onUpdate( function() { self.render() } ); +}; + +Rickshaw.namespace('Rickshaw.Graph.Axis.X'); + +Rickshaw.Graph.Axis.X = function(args) { + + var self = this; + var berthRate = 0.10; + + this.initialize = function(args) { + + this.graph = args.graph; + this.orientation = args.orientation || 'top'; + + this.pixelsPerTick = args.pixelsPerTick || 75; + if (args.ticks) this.staticTicks = args.ticks; + if (args.tickValues) this.tickValues = args.tickValues; + + this.tickSize = args.tickSize || 4; + this.ticksTreatment = args.ticksTreatment || 'plain'; + + if (args.element) { + + this.element = args.element; + this._discoverSize(args.element, args); + + this.vis = d3.select(args.element) + .append("svg:svg") + .attr('height', this.height) + .attr('width', this.width) + .attr('class', 'rickshaw_graph x_axis_d3'); + + this.element = this.vis[0][0]; + this.element.style.position = 'relative'; + + this.setSize({ width: args.width, height: args.height }); + + } else { + this.vis = this.graph.vis; + } + + this.graph.onUpdate( function() { self.render() } ); + }; + + this.setSize = function(args) { + + args = args || {}; + if (!this.element) return; + + this._discoverSize(this.element.parentNode, args); + + this.vis + .attr('height', this.height) + .attr('width', this.width * (1 + berthRate)); + + var berth = Math.floor(this.width * berthRate / 2); + this.element.style.left = -1 * berth + 'px'; + }; + + this.render = function() { + + if (this.graph.width !== this._renderWidth) this.setSize({ auto: true }); + + var axis = d3.svg.axis().scale(this.graph.x).orient(this.orientation); + axis.tickFormat( args.tickFormat || function(x) { return x } ); + if (this.tickValues) axis.tickValues(this.tickValues); + + this.ticks = this.staticTicks || Math.floor(this.graph.width / this.pixelsPerTick); + + var berth = Math.floor(this.width * berthRate / 2) || 0; + var transform; + + if (this.orientation == 'top') { + var yOffset = this.height || this.graph.height; + transform = 'translate(' + berth + ',' + yOffset + ')'; + } else { + transform = 'translate(' + berth + ', 0)'; + } + + if (this.element) { + this.vis.selectAll('*').remove(); + } + + this.vis + .append("svg:g") + .attr("class", ["x_ticks_d3", this.ticksTreatment].join(" ")) + .attr("transform", transform) + .call(axis.ticks(this.ticks).tickSubdivide(0).tickSize(this.tickSize)); + + var gridSize = (this.orientation == 'bottom' ? 1 : -1) * this.graph.height; + + this.graph.vis + .append("svg:g") + .attr("class", "x_grid_d3") + .call(axis.ticks(this.ticks).tickSubdivide(0).tickSize(gridSize)); + + this._renderHeight = this.graph.height; + }; + + this._discoverSize = function(element, args) { + + if (typeof window !== 'undefined') { + + var style = window.getComputedStyle(element, null); + var elementHeight = parseInt(style.getPropertyValue('height'), 10); + + if (!args.auto) { + var elementWidth = parseInt(style.getPropertyValue('width'), 10); + } + } + + this.width = (args.width || elementWidth || this.graph.width) * (1 + berthRate); + this.height = args.height || elementHeight || 40; + }; + + this.initialize(args); +}; + +Rickshaw.namespace('Rickshaw.Graph.Axis.Y'); + +Rickshaw.Graph.Axis.Y = Rickshaw.Class.create( { + + initialize: function(args) { + + this.graph = args.graph; + this.orientation = args.orientation || 'right'; + + this.pixelsPerTick = args.pixelsPerTick || 75; + if (args.ticks) this.staticTicks = args.ticks; + if (args.tickValues) this.tickValues = args.tickValues; + + this.tickSize = args.tickSize || 4; + this.ticksTreatment = args.ticksTreatment || 'plain'; + + this.tickFormat = args.tickFormat || function(y) { return y }; + + this.berthRate = 0.10; + + if (args.element) { + + this.element = args.element; + this.vis = d3.select(args.element) + .append("svg:svg") + .attr('class', 'rickshaw_graph y_axis'); + + this.element = this.vis[0][0]; + this.element.style.position = 'relative'; + + this.setSize({ width: args.width, height: args.height }); + + } else { + this.vis = this.graph.vis; + } + + var self = this; + this.graph.onUpdate( function() { self.render() } ); + }, + + setSize: function(args) { + + args = args || {}; + + if (!this.element) return; + + if (typeof window !== 'undefined') { + + var style = window.getComputedStyle(this.element.parentNode, null); + var elementWidth = parseInt(style.getPropertyValue('width'), 10); + + if (!args.auto) { + var elementHeight = parseInt(style.getPropertyValue('height'), 10); + } + } + + this.width = args.width || elementWidth || this.graph.width * this.berthRate; + this.height = args.height || elementHeight || this.graph.height; + + this.vis + .attr('width', this.width) + .attr('height', this.height * (1 + this.berthRate)); + + var berth = this.height * this.berthRate; + + if (this.orientation == 'left') { + this.element.style.top = -1 * berth + 'px'; + } + }, + + render: function() { + + if (this.graph.height !== this._renderHeight) this.setSize({ auto: true }); + + this.ticks = this.staticTicks || Math.floor(this.graph.height / this.pixelsPerTick); + + var axis = this._drawAxis(this.graph.y); + + this._drawGrid(axis); + + this._renderHeight = this.graph.height; + }, + + _drawAxis: function(scale) { + var axis = d3.svg.axis().scale(scale).orient(this.orientation); + axis.tickFormat(this.tickFormat); + if (this.tickValues) axis.tickValues(this.tickValues); + + if (this.orientation == 'left') { + var berth = this.height * this.berthRate; + var transform = 'translate(' + this.width + ', ' + berth + ')'; + } + + if (this.element) { + this.vis.selectAll('*').remove(); + } + + this.vis + .append("svg:g") + .attr("class", ["y_ticks", this.ticksTreatment].join(" ")) + .attr("transform", transform) + .call(axis.ticks(this.ticks).tickSubdivide(0).tickSize(this.tickSize)); + + return axis; + }, + + _drawGrid: function(axis) { + var gridSize = (this.orientation == 'right' ? 1 : -1) * this.graph.width; + + this.graph.vis + .append("svg:g") + .attr("class", "y_grid") + .call(axis.ticks(this.ticks).tickSubdivide(0).tickSize(gridSize)); + } +} ); +Rickshaw.namespace('Rickshaw.Graph.Axis.Y.Scaled'); + +Rickshaw.Graph.Axis.Y.Scaled = Rickshaw.Class.create( Rickshaw.Graph.Axis.Y, { + + initialize: function($super, args) { + + if (typeof(args.scale) === 'undefined') { + throw new Error('Scaled requires scale'); + } + + this.scale = args.scale; + + if (typeof(args.grid) === 'undefined') { + this.grid = true; + } else { + this.grid = args.grid; + } + + $super(args); + + }, + + _drawAxis: function($super, scale) { + // make a copy of the custom scale, adjust the range to match the graph's scale + var adjustedScale = this.scale.copy().range(scale.range()); + + return $super(adjustedScale); + }, + + _drawGrid: function($super, axis) { + if (this.grid) { + // only draw the axis if the grid option is true + $super(axis); + } + } +} ); +Rickshaw.namespace('Rickshaw.Graph.Behavior.Series.Highlight'); + +Rickshaw.Graph.Behavior.Series.Highlight = function(args) { + + this.graph = args.graph; + this.legend = args.legend; + + var self = this; + + var colorSafe = {}; + var activeLine = null; + + var disabledColor = args.disabledColor || function(seriesColor) { + return d3.interpolateRgb(seriesColor, d3.rgb('#d8d8d8'))(0.8).toString(); + }; + + this.addHighlightEvents = function (l) { + + l.element.addEventListener( 'mouseover', function(e) { + + if (activeLine) return; + else activeLine = l; + + self.legend.lines.forEach( function(line, index) { + + if (l === line) { + + // if we're not in a stacked renderer bring active line to the top + if (index > 0 && self.graph.renderer.unstack && (line.series.renderer ? line.series.renderer.unstack : true)) { + + var seriesIndex = self.graph.series.length - index - 1; + line.originalIndex = seriesIndex; + + var series = self.graph.series.splice(seriesIndex, 1)[0]; + self.graph.series.push(series); + } + return; + } + + colorSafe[line.series.name] = colorSafe[line.series.name] || line.series.color; + line.series.color = disabledColor(line.series.color); + + } ); + + self.graph.update(); + + }, false ); + + l.element.addEventListener( 'mouseout', function(e) { + + if (!activeLine) return; + else activeLine = null; + + self.legend.lines.forEach( function(line) { + + // return reordered series to its original place + if (l === line && line.hasOwnProperty('originalIndex')) { + + var series = self.graph.series.pop(); + self.graph.series.splice(line.originalIndex, 0, series); + delete line.originalIndex; + } + + if (colorSafe[line.series.name]) { + line.series.color = colorSafe[line.series.name]; + } + } ); + + self.graph.update(); + + }, false ); + }; + + if (this.legend) { + this.legend.lines.forEach( function(l) { + self.addHighlightEvents(l); + } ); + } + +}; +Rickshaw.namespace('Rickshaw.Graph.Behavior.Series.Order'); + +Rickshaw.Graph.Behavior.Series.Order = function(args) { + + this.graph = args.graph; + this.legend = args.legend; + + var self = this; + + if (typeof window.$ == 'undefined') { + throw "couldn't find jQuery at window.$"; + } + + if (typeof window.$.ui == 'undefined') { + throw "couldn't find jQuery UI at window.$.ui"; + } + + $(function() { + $(self.legend.list).sortable( { + containment: 'parent', + tolerance: 'pointer', + update: function( event, ui ) { + var series = []; + $(self.legend.list).find('li').each( function(index, item) { + if (!item.series) return; + series.push(item.series); + } ); + + for (var i = self.graph.series.length - 1; i >= 0; i--) { + self.graph.series[i] = series.shift(); + } + + self.graph.update(); + } + } ); + $(self.legend.list).disableSelection(); + }); + + //hack to make jquery-ui sortable behave + this.graph.onUpdate( function() { + var h = window.getComputedStyle(self.legend.element).height; + self.legend.element.style.height = h; + } ); +}; +Rickshaw.namespace('Rickshaw.Graph.Behavior.Series.Toggle'); + +Rickshaw.Graph.Behavior.Series.Toggle = function(args) { + + this.graph = args.graph; + this.legend = args.legend; + + var self = this; + + this.addAnchor = function(line) { + var anchor = document.createElement('a'); + anchor.innerHTML = '✔'; + anchor.classList.add('action'); + line.element.insertBefore(anchor, line.element.firstChild); + + anchor.onclick = function(e) { + if (line.series.disabled) { + line.series.enable(); + line.element.classList.remove('disabled'); + } else { + if (this.graph.series.filter(function(s) { return !s.disabled }).length <= 1) return; + line.series.disable(); + line.element.classList.add('disabled'); + } + + }.bind(this); + + var label = line.element.getElementsByTagName('span')[0]; + label.onclick = function(e){ + + var disableAllOtherLines = line.series.disabled; + if ( ! disableAllOtherLines ) { + for ( var i = 0; i < self.legend.lines.length; i++ ) { + var l = self.legend.lines[i]; + if ( line.series === l.series ) { + // noop + } else if ( l.series.disabled ) { + // noop + } else { + disableAllOtherLines = true; + break; + } + } + } + + // show all or none + if ( disableAllOtherLines ) { + + // these must happen first or else we try ( and probably fail ) to make a no line graph + line.series.enable(); + line.element.classList.remove('disabled'); + + self.legend.lines.forEach(function(l){ + if ( line.series === l.series ) { + // noop + } else { + l.series.disable(); + l.element.classList.add('disabled'); + } + }); + + } else { + + self.legend.lines.forEach(function(l){ + l.series.enable(); + l.element.classList.remove('disabled'); + }); + + } + + }; + + }; + + if (this.legend) { + + if (typeof $ != 'undefined' && $(this.legend.list).sortable) { + + $(this.legend.list).sortable( { + start: function(event, ui) { + ui.item.bind('no.onclick', + function(event) { + event.preventDefault(); + } + ); + }, + stop: function(event, ui) { + setTimeout(function(){ + ui.item.unbind('no.onclick'); + }, 250); + } + }); + } + + this.legend.lines.forEach( function(l) { + self.addAnchor(l); + } ); + } + + this._addBehavior = function() { + + this.graph.series.forEach( function(s) { + + s.disable = function() { + + if (self.graph.series.length <= 1) { + throw('only one series left'); + } + + s.disabled = true; + self.graph.update(); + }; + + s.enable = function() { + s.disabled = false; + self.graph.update(); + }; + } ); + }; + this._addBehavior(); + + this.updateBehaviour = function () { this._addBehavior() }; + +}; +Rickshaw.namespace('Rickshaw.Graph.HoverDetail'); + +Rickshaw.Graph.HoverDetail = Rickshaw.Class.create({ + + initialize: function(args) { + + var graph = this.graph = args.graph; + + this.xFormatter = args.xFormatter || function(x) { + return new Date( x * 1000 ).toUTCString(); + }; + + this.yFormatter = args.yFormatter || function(y) { + return y === null ? y : y.toFixed(2); + }; + + var element = this.element = document.createElement('div'); + element.className = 'detail'; + + this.visible = true; + graph.element.appendChild(element); + + this.lastEvent = null; + this._addListeners(); + + this.onShow = args.onShow; + this.onHide = args.onHide; + this.onRender = args.onRender; + + this.formatter = args.formatter || this.formatter; + + }, + + formatter: function(series, x, y, formattedX, formattedY, d) { + return series.name + ': ' + formattedY; + }, + + update: function(e) { + + e = e || this.lastEvent; + if (!e) return; + this.lastEvent = e; + + if (!e.target.nodeName.match(/^(path|svg|rect|circle)$/)) return; + + var graph = this.graph; + + var eventX = e.offsetX || e.layerX; + var eventY = e.offsetY || e.layerY; + + var j = 0; + var points = []; + var nearestPoint; + + this.graph.series.active().forEach( function(series) { + + var data = this.graph.stackedData[j++]; + + if (!data.length) + return; + + var domainX = graph.x.invert(eventX); + + var domainIndexScale = d3.scale.linear() + .domain([data[0].x, data.slice(-1)[0].x]) + .range([0, data.length - 1]); + + var approximateIndex = Math.round(domainIndexScale(domainX)); + if (approximateIndex == data.length - 1) approximateIndex--; + + var dataIndex = Math.min(approximateIndex || 0, data.length - 1); + + for (var i = approximateIndex; i < data.length - 1;) { + + if (!data[i] || !data[i + 1]) break; + + if (data[i].x <= domainX && data[i + 1].x > domainX) { + dataIndex = Math.abs(domainX - data[i].x) < Math.abs(domainX - data[i + 1].x) ? i : i + 1; + break; + } + + if (data[i + 1].x <= domainX) { i++ } else { i-- } + } + + if (dataIndex < 0) dataIndex = 0; + var value = data[dataIndex]; + + var distance = Math.sqrt( + Math.pow(Math.abs(graph.x(value.x) - eventX), 2) + + Math.pow(Math.abs(graph.y(value.y + value.y0) - eventY), 2) + ); + + var xFormatter = series.xFormatter || this.xFormatter; + var yFormatter = series.yFormatter || this.yFormatter; + + var point = { + formattedXValue: xFormatter(value.x), + formattedYValue: yFormatter(series.scale ? series.scale.invert(value.y) : value.y), + series: series, + value: value, + distance: distance, + order: j, + name: series.name + }; + + if (!nearestPoint || distance < nearestPoint.distance) { + nearestPoint = point; + } + + points.push(point); + + }, this ); + + if (!nearestPoint) + return; + + nearestPoint.active = true; + + var domainX = nearestPoint.value.x; + var formattedXValue = nearestPoint.formattedXValue; + + this.element.innerHTML = ''; + this.element.style.left = graph.x(domainX) + 'px'; + + this.visible && this.render( { + points: points, + detail: points, // for backwards compatibility + mouseX: eventX, + mouseY: eventY, + formattedXValue: formattedXValue, + domainX: domainX + } ); + }, + + hide: function() { + this.visible = false; + this.element.classList.add('inactive'); + + if (typeof this.onHide == 'function') { + this.onHide(); + } + }, + + show: function() { + this.visible = true; + this.element.classList.remove('inactive'); + + if (typeof this.onShow == 'function') { + this.onShow(); + } + }, + + render: function(args) { + + var graph = this.graph; + var points = args.points; + var point = points.filter( function(p) { return p.active } ).shift(); + + if (point.value.y === null) return; + + var formattedXValue = point.formattedXValue; + var formattedYValue = point.formattedYValue; + + this.element.innerHTML = ''; + this.element.style.left = graph.x(point.value.x) + 'px'; + + var xLabel = document.createElement('div'); + + xLabel.className = 'x_label'; + xLabel.innerHTML = formattedXValue; + this.element.appendChild(xLabel); + + var item = document.createElement('div'); + + item.className = 'item'; + + // invert the scale if this series displays using a scale + var series = point.series; + var actualY = series.scale ? series.scale.invert(point.value.y) : point.value.y; + + item.innerHTML = this.formatter(series, point.value.x, actualY, formattedXValue, formattedYValue, point); + item.style.top = this.graph.y(point.value.y0 + point.value.y) + 'px'; + + this.element.appendChild(item); + + var dot = document.createElement('div'); + + dot.className = 'dot'; + dot.style.top = item.style.top; + dot.style.borderColor = series.color; + + this.element.appendChild(dot); + + if (point.active) { + item.className = 'item active'; + dot.className = 'dot active'; + } + + this.show(); + + if (typeof this.onRender == 'function') { + this.onRender(args); + } + }, + + _addListeners: function() { + + this.graph.element.addEventListener( + 'mousemove', + function(e) { + this.visible = true; + this.update(e); + }.bind(this), + false + ); + + this.graph.onUpdate( function() { this.update() }.bind(this) ); + + this.graph.element.addEventListener( + 'mouseout', + function(e) { + if (e.relatedTarget && !(e.relatedTarget.compareDocumentPosition(this.graph.element) & Node.DOCUMENT_POSITION_CONTAINS)) { + this.hide(); + } + }.bind(this), + false + ); + } +}); + +Rickshaw.namespace('Rickshaw.Graph.JSONP'); + +Rickshaw.Graph.JSONP = Rickshaw.Class.create( Rickshaw.Graph.Ajax, { + + request: function() { + + $.ajax( { + url: this.dataURL, + dataType: 'jsonp', + success: this.success.bind(this), + error: this.error.bind(this) + } ); + } +} ); +Rickshaw.namespace('Rickshaw.Graph.Legend'); + +Rickshaw.Graph.Legend = function(args) { + + var element = this.element = args.element; + var graph = this.graph = args.graph; + + var self = this; + + element.classList.add('rickshaw_legend'); + + var list = this.list = document.createElement('ul'); + element.appendChild(list); + + var series = graph.series + .map( function(s) { return s } ); + + if (!args.naturalOrder) { + series = series.reverse(); + } + + this.lines = []; + + this.addLine = function (series) { + var line = document.createElement('li'); + line.className = 'line'; + if (series.disabled) { + line.className += ' disabled'; + } + + var swatch = document.createElement('div'); + swatch.className = 'swatch'; + swatch.style.backgroundColor = series.color; + + line.appendChild(swatch); + + var label = document.createElement('span'); + label.className = 'label'; + label.innerHTML = series.name; + + line.appendChild(label); + list.appendChild(line); + + line.series = series; + + if (series.noLegend) { + line.style.display = 'none'; + } + + var _line = { element: line, series: series }; + if (self.shelving) { + self.shelving.addAnchor(_line); + self.shelving.updateBehaviour(); + } + if (self.highlighter) { + self.highlighter.addHighlightEvents(_line); + } + self.lines.push(_line); + }; + + series.forEach( function(s) { + self.addLine(s); + } ); + + graph.onUpdate( function() {} ); +}; +Rickshaw.namespace('Rickshaw.Graph.RangeSlider'); + +Rickshaw.Graph.RangeSlider = Rickshaw.Class.create({ + + initialize: function(args) { + + var element = this.element = args.element; + var graph = this.graph = args.graph; + + this.build(); + + graph.onUpdate( function() { this.update() }.bind(this) ); + }, + + build: function() { + + var element = this.element; + var graph = this.graph; + + var domain = graph.dataDomain(); + + $( function() { + $(element).slider( { + range: true, + min: domain[0], + max: domain[1], + values: [ + domain[0], + domain[1] + ], + slide: function( event, ui ) { + + if (ui.values[1] <= ui.values[0]) return; + + graph.window.xMin = ui.values[0]; + graph.window.xMax = ui.values[1]; + graph.update(); + + var domain = graph.dataDomain(); + + // if we're at an extreme, stick there + if (domain[0] == ui.values[0]) { + graph.window.xMin = undefined; + } + if (domain[1] == ui.values[1]) { + graph.window.xMax = undefined; + } + } + } ); + } ); + + element[0].style.width = graph.width + 'px'; + }, + + update: function() { + + var element = this.element; + var graph = this.graph; + + var values = $(element).slider('option', 'values'); + + var domain = graph.dataDomain(); + + $(element).slider('option', 'min', domain[0]); + $(element).slider('option', 'max', domain[1]); + + if (graph.window.xMin == null) { + values[0] = domain[0]; + } + if (graph.window.xMax == null) { + values[1] = domain[1]; + } + + $(element).slider('option', 'values', values); + } +}); + +Rickshaw.namespace("Rickshaw.Graph.Renderer"); + +Rickshaw.Graph.Renderer = Rickshaw.Class.create( { + + initialize: function(args) { + this.graph = args.graph; + this.tension = args.tension || this.tension; + this.graph.unstacker = this.graph.unstacker || new Rickshaw.Graph.Unstacker( { graph: this.graph } ); + this.configure(args); + }, + + seriesPathFactory: function() { + //implement in subclass + }, + + seriesStrokeFactory: function() { + // implement in subclass + }, + + defaults: function() { + return { + tension: 0.8, + strokeWidth: 2, + unstack: true, + padding: { top: 0.01, right: 0, bottom: 0.01, left: 0 }, + stroke: false, + fill: false + }; + }, + + domain: function(data) { + + var stackedData = data || this.graph.stackedData || this.graph.stackData(); + var firstPoint = stackedData[0][0]; + + if (firstPoint === undefined) { + return { x: [null, null], y: [null, null] }; + } + + var xMin = firstPoint.x; + var xMax = firstPoint.x; + + var yMin = firstPoint.y + firstPoint.y0; + var yMax = firstPoint.y + firstPoint.y0; + + stackedData.forEach( function(series) { + + series.forEach( function(d) { + + if (d.y == null) return; + + var y = d.y + d.y0; + + if (y < yMin) yMin = y; + if (y > yMax) yMax = y; + } ); + + if (series[0].x < xMin) xMin = series[0].x; + if (series[series.length - 1].x > xMax) xMax = series[series.length - 1].x; + } ); + + xMin -= (xMax - xMin) * this.padding.left; + xMax += (xMax - xMin) * this.padding.right; + + yMin = this.graph.min === 'auto' ? yMin : this.graph.min || 0; + yMax = this.graph.max === undefined ? yMax : this.graph.max; + + if (this.graph.min === 'auto' || yMin < 0) { + yMin -= (yMax - yMin) * this.padding.bottom; + } + + if (this.graph.max === undefined) { + yMax += (yMax - yMin) * this.padding.top; + } + + return { x: [xMin, xMax], y: [yMin, yMax] }; + }, + + render: function(args) { + + args = args || {}; + + var graph = this.graph; + var series = args.series || graph.series; + + var vis = args.vis || graph.vis; + vis.selectAll('*').remove(); + + var data = series + .filter(function(s) { return !s.disabled }) + .map(function(s) { return s.stack }); + + var nodes = vis.selectAll("path") + .data(data) + .enter().append("svg:path") + .attr("d", this.seriesPathFactory()); + + var i = 0; + series.forEach( function(series) { + if (series.disabled) return; + series.path = nodes[0][i++]; + this._styleSeries(series); + }, this ); + }, + + _styleSeries: function(series) { + + var fill = this.fill ? series.color : 'none'; + var stroke = this.stroke ? series.color : 'none'; + + series.path.setAttribute('fill', fill); + series.path.setAttribute('stroke', stroke); + series.path.setAttribute('stroke-width', this.strokeWidth); + series.path.setAttribute('class', series.className); + }, + + configure: function(args) { + + args = args || {}; + + Rickshaw.keys(this.defaults()).forEach( function(key) { + + if (!args.hasOwnProperty(key)) { + this[key] = this[key] || this.graph[key] || this.defaults()[key]; + return; + } + + if (typeof this.defaults()[key] == 'object') { + + Rickshaw.keys(this.defaults()[key]).forEach( function(k) { + + this[key][k] = + args[key][k] !== undefined ? args[key][k] : + this[key][k] !== undefined ? this[key][k] : + this.defaults()[key][k]; + }, this ); + + } else { + this[key] = + args[key] !== undefined ? args[key] : + this[key] !== undefined ? this[key] : + this.graph[key] !== undefined ? this.graph[key] : + this.defaults()[key]; + } + + }, this ); + }, + + setStrokeWidth: function(strokeWidth) { + if (strokeWidth !== undefined) { + this.strokeWidth = strokeWidth; + } + }, + + setTension: function(tension) { + if (tension !== undefined) { + this.tension = tension; + } + } +} ); + +Rickshaw.namespace('Rickshaw.Graph.Renderer.Line'); + +Rickshaw.Graph.Renderer.Line = Rickshaw.Class.create( Rickshaw.Graph.Renderer, { + + name: 'line', + + defaults: function($super) { + + return Rickshaw.extend( $super(), { + unstack: true, + fill: false, + stroke: true + } ); + }, + + seriesPathFactory: function() { + + var graph = this.graph; + + var factory = d3.svg.line() + .x( function(d) { return graph.x(d.x) } ) + .y( function(d) { return graph.y(d.y) } ) + .interpolate(this.graph.interpolation).tension(this.tension); + + factory.defined && factory.defined( function(d) { return d.y !== null } ); + return factory; + } +} ); + +Rickshaw.namespace('Rickshaw.Graph.Renderer.Stack'); + +Rickshaw.Graph.Renderer.Stack = Rickshaw.Class.create( Rickshaw.Graph.Renderer, { + + name: 'stack', + + defaults: function($super) { + + return Rickshaw.extend( $super(), { + fill: true, + stroke: false, + unstack: false + } ); + }, + + seriesPathFactory: function() { + + var graph = this.graph; + + var factory = d3.svg.area() + .x( function(d) { return graph.x(d.x) } ) + .y0( function(d) { return graph.y(d.y0) } ) + .y1( function(d) { return graph.y(d.y + d.y0) } ) + .interpolate(this.graph.interpolation).tension(this.tension); + + factory.defined && factory.defined( function(d) { return d.y !== null } ); + return factory; + } +} ); + +Rickshaw.namespace('Rickshaw.Graph.Renderer.Bar'); + +Rickshaw.Graph.Renderer.Bar = Rickshaw.Class.create( Rickshaw.Graph.Renderer, { + + name: 'bar', + + defaults: function($super) { + + var defaults = Rickshaw.extend( $super(), { + gapSize: 0.05, + unstack: false + } ); + + delete defaults.tension; + return defaults; + }, + + initialize: function($super, args) { + args = args || {}; + this.gapSize = args.gapSize || this.gapSize; + $super(args); + }, + + domain: function($super) { + + var domain = $super(); + + var frequentInterval = this._frequentInterval(this.graph.stackedData.slice(-1).shift()); + domain.x[1] += Number(frequentInterval.magnitude); + + return domain; + }, + + barWidth: function(series) { + + var frequentInterval = this._frequentInterval(series.stack); + var barWidth = this.graph.x(series.stack[0].x + frequentInterval.magnitude * (1 - this.gapSize)); + + return barWidth; + }, + + render: function(args) { + + args = args || {}; + + var graph = this.graph; + var series = args.series || graph.series; + + var vis = args.vis || graph.vis; + vis.selectAll('*').remove(); + + var barWidth = this.barWidth(series.active()[0]); + var barXOffset = 0; + + var activeSeriesCount = series.filter( function(s) { return !s.disabled; } ).length; + var seriesBarWidth = this.unstack ? barWidth / activeSeriesCount : barWidth; + + var transform = function(d) { + // add a matrix transform for negative values + var matrix = [ 1, 0, 0, (d.y < 0 ? -1 : 1), 0, (d.y < 0 ? graph.y.magnitude(Math.abs(d.y)) * 2 : 0) ]; + return "matrix(" + matrix.join(',') + ")"; + }; + + series.forEach( function(series) { + + if (series.disabled) return; + + var barWidth = this.barWidth(series); + + var nodes = vis.selectAll("path") + .data(series.stack.filter( function(d) { return d.y !== null } )) + .enter().append("svg:rect") + .attr("x", function(d) { return graph.x(d.x) + barXOffset }) + .attr("y", function(d) { return (graph.y(d.y0 + Math.abs(d.y))) * (d.y < 0 ? -1 : 1 ) }) + .attr("width", seriesBarWidth) + .attr("height", function(d) { return graph.y.magnitude(Math.abs(d.y)) }) + .attr("transform", transform); + + Array.prototype.forEach.call(nodes[0], function(n) { + n.setAttribute('fill', series.color); + } ); + + if (this.unstack) barXOffset += seriesBarWidth; + + }, this ); + }, + + _frequentInterval: function(data) { + + var intervalCounts = {}; + + for (var i = 0; i < data.length - 1; i++) { + var interval = data[i + 1].x - data[i].x; + intervalCounts[interval] = intervalCounts[interval] || 0; + intervalCounts[interval]++; + } + + var frequentInterval = { count: 0, magnitude: 1 }; + + Rickshaw.keys(intervalCounts).forEach( function(i) { + if (frequentInterval.count < intervalCounts[i]) { + frequentInterval = { + count: intervalCounts[i], + magnitude: i + }; + } + } ); + + return frequentInterval; + } +} ); + +Rickshaw.namespace('Rickshaw.Graph.Renderer.Area'); + +Rickshaw.Graph.Renderer.Area = Rickshaw.Class.create( Rickshaw.Graph.Renderer, { + + name: 'area', + + defaults: function($super) { + + return Rickshaw.extend( $super(), { + unstack: false, + fill: false, + stroke: false + } ); + }, + + seriesPathFactory: function() { + + var graph = this.graph; + + var factory = d3.svg.area() + .x( function(d) { return graph.x(d.x) } ) + .y0( function(d) { return graph.y(d.y0) } ) + .y1( function(d) { return graph.y(d.y + d.y0) } ) + .interpolate(graph.interpolation).tension(this.tension); + + factory.defined && factory.defined( function(d) { return d.y !== null } ); + return factory; + }, + + seriesStrokeFactory: function() { + + var graph = this.graph; + + var factory = d3.svg.line() + .x( function(d) { return graph.x(d.x) } ) + .y( function(d) { return graph.y(d.y + d.y0) } ) + .interpolate(graph.interpolation).tension(this.tension); + + factory.defined && factory.defined( function(d) { return d.y !== null } ); + return factory; + }, + + render: function() { + + var graph = this.graph; + + graph.vis.selectAll('*').remove(); + + // insert or stacked areas so strokes lay on top of areas + var method = this.unstack ? 'append' : 'insert'; + + var nodes = graph.vis.selectAll("path") + .data(this.graph.stackedData) + .enter()[method]("svg:g", 'g'); + + nodes.append("svg:path") + .attr("d", this.seriesPathFactory()) + .attr("class", 'area'); + + if (this.stroke) { + nodes.append("svg:path") + .attr("d", this.seriesStrokeFactory()) + .attr("class", 'line'); + } + + var i = 0; + graph.series.forEach( function(series) { + if (series.disabled) return; + series.path = nodes[0][i++]; + this._styleSeries(series); + }, this ); + }, + + _styleSeries: function(series) { + + if (!series.path) return; + + d3.select(series.path).select('.area') + .attr('fill', series.color); + + if (this.stroke) { + d3.select(series.path).select('.line') + .attr('fill', 'none') + .attr('stroke', series.stroke || d3.interpolateRgb(series.color, 'black')(0.125)) + .attr('stroke-width', this.strokeWidth); + } + + if (series.className) { + series.path.setAttribute('class', series.className); + } + } +} ); + +Rickshaw.namespace('Rickshaw.Graph.Renderer.ScatterPlot'); + +Rickshaw.Graph.Renderer.ScatterPlot = Rickshaw.Class.create( Rickshaw.Graph.Renderer, { + + name: 'scatterplot', + + defaults: function($super) { + + return Rickshaw.extend( $super(), { + unstack: true, + fill: true, + stroke: false, + padding:{ top: 0.01, right: 0.01, bottom: 0.01, left: 0.01 }, + dotSize: 4 + } ); + }, + + initialize: function($super, args) { + $super(args); + }, + + render: function(args) { + + args = args || {}; + + var graph = this.graph; + + var series = args.series || graph.series; + var vis = args.vis || graph.vis; + + var dotSize = this.dotSize; + + vis.selectAll('*').remove(); + + series.forEach( function(series) { + + if (series.disabled) return; + + var nodes = vis.selectAll("path") + .data(series.stack.filter( function(d) { return d.y !== null } )) + .enter().append("svg:circle") + .attr("cx", function(d) { return graph.x(d.x) }) + .attr("cy", function(d) { return graph.y(d.y) }) + .attr("r", function(d) { return ("r" in d) ? d.r : dotSize}); + + Array.prototype.forEach.call(nodes[0], function(n) { + n.setAttribute('fill', series.color); + } ); + + }, this ); + } +} ); +Rickshaw.namespace('Rickshaw.Graph.Renderer.Multi'); + +Rickshaw.Graph.Renderer.Multi = Rickshaw.Class.create( Rickshaw.Graph.Renderer, { + + name: 'multi', + + initialize: function($super, args) { + + $super(args); + }, + + defaults: function($super) { + + return Rickshaw.extend( $super(), { + unstack: true, + fill: false, + stroke: true + } ); + }, + + domain: function($super) { + + this.graph.stackData(); + + var domains = []; + + var groups = this._groups(); + this._stack(groups); + + groups.forEach( function(group) { + + var data = group.series + .filter( function(s) { return !s.disabled } ) + .map( function(s) { return s.stack }); + + if (!data.length) return; + + var domain = $super(data); + domains.push(domain); + }); + + var xMin = d3.min(domains.map( function(d) { return d.x[0] } )); + var xMax = d3.max(domains.map( function(d) { return d.x[1] } )); + var yMin = d3.min(domains.map( function(d) { return d.y[0] } )); + var yMax = d3.max(domains.map( function(d) { return d.y[1] } )); + + return { x: [xMin, xMax], y: [yMin, yMax] }; + }, + + _groups: function() { + + var graph = this.graph; + + var renderGroups = {}; + + graph.series.forEach( function(series) { + + if (series.disabled) return; + + if (!renderGroups[series.renderer]) { + + var ns = "http://www.w3.org/2000/svg"; + var vis = document.createElementNS(ns, 'g'); + + graph.vis[0][0].appendChild(vis); + + var renderer = graph._renderers[series.renderer]; + + renderGroups[series.renderer] = { + renderer: renderer, + series: [], + vis: d3.select(vis) + }; + } + + renderGroups[series.renderer].series.push(series); + + }, this); + + var groups = []; + + Object.keys(renderGroups).forEach( function(key) { + var group = renderGroups[key]; + groups.push(group); + }); + + return groups; + }, + + _stack: function(groups) { + + groups.forEach( function(group) { + + var series = group.series + .filter( function(series) { return !series.disabled } ); + + var data = series + .map( function(series) { return series.stack } ); + + if (!group.renderer.unstack) { + + var layout = d3.layout.stack(); + var stackedData = Rickshaw.clone(layout(data)); + + series.forEach( function(series, index) { + series._stack = Rickshaw.clone(stackedData[index]); + }); + } + + }, this ); + + return groups; + + }, + + render: function() { + + this.graph.series.forEach( function(series) { + if (!series.renderer) { + throw new Error("Each series needs a renderer for graph 'multi' renderer"); + } + }); + + this.graph.vis.selectAll('*').remove(); + + var groups = this._groups(); + groups = this._stack(groups); + + groups.forEach( function(group) { + + var series = group.series + .filter( function(series) { return !series.disabled } ); + + group.renderer.render({ series: series, vis: group.vis }); + series.forEach(function(s) { s.stack = s._stack || s.stack || s.data; }); + }); + } + +} ); +Rickshaw.namespace('Rickshaw.Graph.Renderer.LinePlot'); + +Rickshaw.Graph.Renderer.LinePlot = Rickshaw.Class.create( Rickshaw.Graph.Renderer, { + + name: 'lineplot', + + defaults: function($super) { + + return Rickshaw.extend( $super(), { + unstack: true, + fill: false, + stroke: true, + padding:{ top: 0.01, right: 0.01, bottom: 0.01, left: 0.01 }, + dotSize: 3, + strokeWidth: 2 + } ); + }, + + initialize: function($super, args) { + $super(args); + }, + + seriesPathFactory: function() { + + var graph = this.graph; + + var factory = d3.svg.line() + .x( function(d) { return graph.x(d.x) } ) + .y( function(d) { return graph.y(d.y) } ) + .interpolate(this.graph.interpolation).tension(this.tension); + + factory.defined && factory.defined( function(d) { return d.y !== null } ); + return factory; + }, + + _renderDots: function() { + + var graph = this.graph; + + graph.series.forEach(function(series) { + + if (series.disabled) return; + + var nodes = graph.vis.selectAll("x") + .data(series.stack.filter( function(d) { return d.y !== null } )) + .enter().append("svg:circle") + .attr("cx", function(d) { return graph.x(d.x) }) + .attr("cy", function(d) { return graph.y(d.y) }) + .attr("r", function(d) { return ("r" in d) ? d.r : graph.renderer.dotSize}); + + Array.prototype.forEach.call(nodes[0], function(n) { + if (!n) return; + n.setAttribute('data-color', series.color); + n.setAttribute('fill', 'white'); + n.setAttribute('stroke', series.color); + n.setAttribute('stroke-width', this.strokeWidth); + + }.bind(this)); + + }, this); + }, + + _renderLines: function() { + + var graph = this.graph; + + var nodes = graph.vis.selectAll("path") + .data(this.graph.stackedData) + .enter().append("svg:path") + .attr("d", this.seriesPathFactory()); + + var i = 0; + graph.series.forEach(function(series) { + if (series.disabled) return; + series.path = nodes[0][i++]; + this._styleSeries(series); + }, this); + }, + + render: function() { + + var graph = this.graph; + + graph.vis.selectAll('*').remove(); + + this._renderLines(); + this._renderDots(); + } +} ); + +Rickshaw.namespace('Rickshaw.Graph.Smoother'); + +Rickshaw.Graph.Smoother = Rickshaw.Class.create({ + + initialize: function(args) { + + this.graph = args.graph; + this.element = args.element; + this.aggregationScale = 1; + + this.build(); + + this.graph.stackData.hooks.data.push( { + name: 'smoother', + orderPosition: 50, + f: this.transformer.bind(this) + } ); + }, + + build: function() { + + var self = this; + + if (this.element) { + $( function() { + $(self.element).slider( { + min: 1, + max: 100, + slide: function( event, ui ) { + self.setScale(ui.value); + self.graph.update(); + } + } ); + } ); + } + }, + + setScale: function(scale) { + + if (scale < 1) { + throw "scale out of range: " + scale; + } + + this.aggregationScale = scale; + this.graph.update(); + }, + + transformer: function(data) { + + if (this.aggregationScale == 1) return data; + + var aggregatedData = []; + + data.forEach( function(seriesData) { + + var aggregatedSeriesData = []; + + while (seriesData.length) { + + var avgX = 0, avgY = 0; + var slice = seriesData.splice(0, this.aggregationScale); + + slice.forEach( function(d) { + avgX += d.x / slice.length; + avgY += d.y / slice.length; + } ); + + aggregatedSeriesData.push( { x: avgX, y: avgY } ); + } + + aggregatedData.push(aggregatedSeriesData); + + }.bind(this) ); + + return aggregatedData; + } +}); + +Rickshaw.namespace('Rickshaw.Graph.Unstacker'); + +Rickshaw.Graph.Unstacker = function(args) { + + this.graph = args.graph; + var self = this; + + this.graph.stackData.hooks.after.push( { + name: 'unstacker', + f: function(data) { + + if (!self.graph.renderer.unstack) return data; + + data.forEach( function(seriesData) { + seriesData.forEach( function(d) { + d.y0 = 0; + } ); + } ); + + return data; + } + } ); +}; + +Rickshaw.namespace('Rickshaw.Series'); + +Rickshaw.Series = Rickshaw.Class.create( Array, { + + initialize: function (data, palette, options) { + + options = options || {}; + + this.palette = new Rickshaw.Color.Palette(palette); + + this.timeBase = typeof(options.timeBase) === 'undefined' ? + Math.floor(new Date().getTime() / 1000) : + options.timeBase; + + var timeInterval = typeof(options.timeInterval) == 'undefined' ? + 1000 : + options.timeInterval; + + this.setTimeInterval(timeInterval); + + if (data && (typeof(data) == "object") && Array.isArray(data)) { + data.forEach( function(item) { this.addItem(item) }, this ); + } + }, + + addItem: function(item) { + + if (typeof(item.name) === 'undefined') { + throw('addItem() needs a name'); + } + + item.color = (item.color || this.palette.color(item.name)); + item.data = (item.data || []); + + // backfill, if necessary + if ((item.data.length === 0) && this.length && (this.getIndex() > 0)) { + this[0].data.forEach( function(plot) { + item.data.push({ x: plot.x, y: 0 }); + } ); + } else if (item.data.length === 0) { + item.data.push({ x: this.timeBase - (this.timeInterval || 0), y: 0 }); + } + + this.push(item); + + if (this.legend) { + this.legend.addLine(this.itemByName(item.name)); + } + }, + + addData: function(data, x) { + + var index = this.getIndex(); + + Rickshaw.keys(data).forEach( function(name) { + if (! this.itemByName(name)) { + this.addItem({ name: name }); + } + }, this ); + + this.forEach( function(item) { + item.data.push({ + x: x || (index * this.timeInterval || 1) + this.timeBase, + y: (data[item.name] || 0) + }); + }, this ); + }, + + getIndex: function () { + return (this[0] && this[0].data && this[0].data.length) ? this[0].data.length : 0; + }, + + itemByName: function(name) { + + for (var i = 0; i < this.length; i++) { + if (this[i].name == name) + return this[i]; + } + }, + + setTimeInterval: function(iv) { + this.timeInterval = iv / 1000; + }, + + setTimeBase: function (t) { + this.timeBase = t; + }, + + dump: function() { + + var data = { + timeBase: this.timeBase, + timeInterval: this.timeInterval, + items: [] + }; + + this.forEach( function(item) { + + var newItem = { + color: item.color, + name: item.name, + data: [] + }; + + item.data.forEach( function(plot) { + newItem.data.push({ x: plot.x, y: plot.y }); + } ); + + data.items.push(newItem); + } ); + + return data; + }, + + load: function(data) { + + if (data.timeInterval) { + this.timeInterval = data.timeInterval; + } + + if (data.timeBase) { + this.timeBase = data.timeBase; + } + + if (data.items) { + data.items.forEach( function(item) { + this.push(item); + if (this.legend) { + this.legend.addLine(this.itemByName(item.name)); + } + + }, this ); + } + } +} ); + +Rickshaw.Series.zeroFill = function(series) { + Rickshaw.Series.fill(series, 0); +}; + +Rickshaw.Series.fill = function(series, fill) { + + var x; + var i = 0; + + var data = series.map( function(s) { return s.data } ); + + while ( i < Math.max.apply(null, data.map( function(d) { return d.length } )) ) { + + x = Math.min.apply( null, + data + .filter(function(d) { return d[i] }) + .map(function(d) { return d[i].x }) + ); + + data.forEach( function(d) { + if (!d[i] || d[i].x != x) { + d.splice(i, 0, { x: x, y: fill }); + } + } ); + + i++; + } +}; + +Rickshaw.namespace('Rickshaw.Series.FixedDuration'); + +Rickshaw.Series.FixedDuration = Rickshaw.Class.create(Rickshaw.Series, { + + initialize: function (data, palette, options) { + + options = options || {}; + + if (typeof(options.timeInterval) === 'undefined') { + throw new Error('FixedDuration series requires timeInterval'); + } + + if (typeof(options.maxDataPoints) === 'undefined') { + throw new Error('FixedDuration series requires maxDataPoints'); + } + + this.palette = new Rickshaw.Color.Palette(palette); + this.timeBase = typeof(options.timeBase) === 'undefined' ? Math.floor(new Date().getTime() / 1000) : options.timeBase; + this.setTimeInterval(options.timeInterval); + + if (this[0] && this[0].data && this[0].data.length) { + this.currentSize = this[0].data.length; + this.currentIndex = this[0].data.length; + } else { + this.currentSize = 0; + this.currentIndex = 0; + } + + this.maxDataPoints = options.maxDataPoints; + + + if (data && (typeof(data) == "object") && Array.isArray(data)) { + data.forEach( function (item) { this.addItem(item) }, this ); + this.currentSize += 1; + this.currentIndex += 1; + } + + // reset timeBase for zero-filled values if needed + this.timeBase -= (this.maxDataPoints - this.currentSize) * this.timeInterval; + + // zero-fill up to maxDataPoints size if we don't have that much data yet + if ((typeof(this.maxDataPoints) !== 'undefined') && (this.currentSize < this.maxDataPoints)) { + for (var i = this.maxDataPoints - this.currentSize - 1; i > 1; i--) { + this.currentSize += 1; + this.currentIndex += 1; + this.forEach( function (item) { + item.data.unshift({ x: ((i-1) * this.timeInterval || 1) + this.timeBase, y: 0, i: i }); + }, this ); + } + } + }, + + addData: function($super, data, x) { + + $super(data, x); + + this.currentSize += 1; + this.currentIndex += 1; + + if (this.maxDataPoints !== undefined) { + while (this.currentSize > this.maxDataPoints) { + this.dropData(); + } + } + }, + + dropData: function() { + + this.forEach(function(item) { + item.data.splice(0, 1); + } ); + + this.currentSize -= 1; + }, + + getIndex: function () { + return this.currentIndex; + } +} ); + diff --git a/cat-home/src/main/webapp/js/rickshaw.min.js b/cat-home/src/main/webapp/js/rickshaw.min.js new file mode 100644 index 0000000000000000000000000000000000000000..fac600ff7b99d45b8f4eb23eb30b578f6dbf931a --- /dev/null +++ b/cat-home/src/main/webapp/js/rickshaw.min.js @@ -0,0 +1,2 @@ +var Rickshaw={namespace:function(namespace,obj){var parts=namespace.split(".");var parent=Rickshaw;for(var i=1,length=parts.length;i=3){if(s.data[2].xthis.window.xMax)isInRange=false;return isInRange}return true};this.onUpdate=function(callback){this.updateCallbacks.push(callback)};this.registerRenderer=function(renderer){this._renderers=this._renderers||{};this._renderers[renderer.name]=renderer};this.configure=function(args){if(args.width||args.height){this.setSize(args)}Rickshaw.keys(this.defaults).forEach(function(k){this[k]=k in args?args[k]:k in this?this[k]:this.defaults[k]},this);this.setRenderer(args.renderer||this.renderer.name,args)};this.setRenderer=function(r,args){if(typeof r=="function"){this.renderer=new r({graph:self});this.registerRenderer(this.renderer)}else{if(!this._renderers[r]){throw"couldn't find renderer "+r}this.renderer=this._renderers[r]}if(typeof args=="object"){this.renderer.configure(args)}};this.setSize=function(args){args=args||{};if(typeof window!==undefined){var style=window.getComputedStyle(this.element,null);var elementWidth=parseInt(style.getPropertyValue("width"),10);var elementHeight=parseInt(style.getPropertyValue("height"),10)}this.width=args.width||elementWidth||400;this.height=args.height||elementHeight||250;this.vis&&this.vis.attr("width",this.width).attr("height",this.height)};this.initialize(args)};Rickshaw.namespace("Rickshaw.Fixtures.Color");Rickshaw.Fixtures.Color=function(){this.schemes={};this.schemes.spectrum14=["#ecb796","#dc8f70","#b2a470","#92875a","#716c49","#d2ed82","#bbe468","#a1d05d","#e7cbe6","#d8aad6","#a888c2","#9dc2d3","#649eb9","#387aa3"].reverse();this.schemes.spectrum2000=["#57306f","#514c76","#646583","#738394","#6b9c7d","#84b665","#a7ca50","#bfe746","#e2f528","#fff726","#ecdd00","#d4b11d","#de8800","#de4800","#c91515","#9a0000","#7b0429","#580839","#31082b"];this.schemes.spectrum2001=["#2f243f","#3c2c55","#4a3768","#565270","#6b6b7c","#72957f","#86ad6e","#a1bc5e","#b8d954","#d3e04e","#ccad2a","#cc8412","#c1521d","#ad3821","#8a1010","#681717","#531e1e","#3d1818","#320a1b"];this.schemes.classic9=["#423d4f","#4a6860","#848f39","#a2b73c","#ddcb53","#c5a32f","#7d5836","#963b20","#7c2626","#491d37","#2f254a"].reverse();this.schemes.httpStatus={503:"#ea5029",502:"#d23f14",500:"#bf3613",410:"#efacea",409:"#e291dc",403:"#f457e8",408:"#e121d2",401:"#b92dae",405:"#f47ceb",404:"#a82a9f",400:"#b263c6",301:"#6fa024",302:"#87c32b",307:"#a0d84c",304:"#28b55c",200:"#1a4f74",206:"#27839f",201:"#52adc9",202:"#7c979f",203:"#a5b8bd",204:"#c1cdd1"};this.schemes.colorwheel=["#b5b6a9","#858772","#785f43","#96557e","#4682b4","#65b9ac","#73c03a","#cb513a"].reverse();this.schemes.cool=["#5e9d2f","#73c03a","#4682b4","#7bc3b8","#a9884e","#c1b266","#a47493","#c09fb5"];this.schemes.munin=["#00cc00","#0066b3","#ff8000","#ffcc00","#330099","#990099","#ccff00","#ff0000","#808080","#008f00","#00487d","#b35a00","#b38f00","#6b006b","#8fb300","#b30000","#bebebe","#80ff80","#80c9ff","#ffc080","#ffe680","#aa80ff","#ee00cc","#ff8080","#666600","#ffbfff","#00ffcc","#cc6699","#999900"]};Rickshaw.namespace("Rickshaw.Fixtures.RandomData");Rickshaw.Fixtures.RandomData=function(timeInterval){var addData;timeInterval=timeInterval||1;var lastRandomValue=200;var timeBase=Math.floor((new Date).getTime()/1e3);this.addData=function(data){var randomValue=Math.random()*100+15+lastRandomValue;var index=data[0].length;var counter=1;data.forEach(function(series){var randomVariance=Math.random()*20;var v=randomValue/25+counter++ +(Math.cos(index*counter*11/960)+2)*15+(Math.cos(index/7)+2)*7+(Math.cos(index/17)+2)*1;series.push({x:index*timeInterval+timeBase,y:v+randomVariance})});lastRandomValue=randomValue*.85};this.removeData=function(data){data.forEach(function(series){series.shift()});timeBase+=timeInterval}};Rickshaw.namespace("Rickshaw.Fixtures.Time");Rickshaw.Fixtures.Time=function(){var tzOffset=(new Date).getTimezoneOffset()*60;var self=this;this.months=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];this.units=[{name:"decade",seconds:86400*365.25*10,formatter:function(d){return parseInt(d.getUTCFullYear()/10,10)*10}},{name:"year",seconds:86400*365.25,formatter:function(d){return d.getUTCFullYear()}},{name:"month",seconds:86400*30.5,formatter:function(d){return self.months[d.getUTCMonth()]}},{name:"week",seconds:86400*7,formatter:function(d){return self.formatDate(d)}},{name:"day",seconds:86400,formatter:function(d){return d.getUTCDate()}},{name:"6 hour",seconds:3600*6,formatter:function(d){return self.formatTime(d)}},{name:"hour",seconds:3600,formatter:function(d){return self.formatTime(d)}},{name:"15 minute",seconds:60*15,formatter:function(d){return self.formatTime(d)}},{name:"minute",seconds:60,formatter:function(d){return d.getUTCMinutes()}},{name:"15 second",seconds:15,formatter:function(d){return d.getUTCSeconds()+"s"}},{name:"second",seconds:1,formatter:function(d){return d.getUTCSeconds()+"s"}}];this.unit=function(unitName){return this.units.filter(function(unit){return unitName==unit.name}).shift()};this.formatDate=function(d){return d3.time.format("%b %e")(d)};this.formatTime=function(d){return d.toUTCString().match(/(\d+:\d+):/)[1]};this.ceil=function(time,unit){var nearFuture;var rounded;if(unit.name=="month"){nearFuture=new Date((time+unit.seconds-1)*1e3);rounded=new Date(0);rounded.setUTCFullYear(nearFuture.getUTCFullYear());rounded.setUTCMonth(nearFuture.getUTCMonth());rounded.setUTCDate(1);rounded.setUTCHours(0);rounded.setUTCMinutes(0);rounded.setUTCSeconds(0);rounded.setUTCMilliseconds(0);return rounded.getTime()/1e3}if(unit.name=="year"){nearFuture=new Date((time+unit.seconds-1)*1e3);rounded=new Date(0);rounded.setUTCFullYear(nearFuture.getUTCFullYear());rounded.setUTCMonth(0);rounded.setUTCDate(1);rounded.setUTCHours(0);rounded.setUTCMinutes(0);rounded.setUTCSeconds(0);rounded.setUTCMilliseconds(0);return rounded.getTime()/1e3}return Math.ceil(time/unit.seconds)*unit.seconds}};Rickshaw.namespace("Rickshaw.Fixtures.Time.Local");Rickshaw.Fixtures.Time.Local=function(){var tzOffset=(new Date).getTimezoneOffset()*60;var self=this;this.months=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];this.units=[{name:"decade",seconds:86400*365.25*10,formatter:function(d){return parseInt(d.getFullYear()/10,10)*10}},{name:"year",seconds:86400*365.25,formatter:function(d){return d.getFullYear()}},{name:"month",seconds:86400*30.5,formatter:function(d){return self.months[d.getMonth()]}},{name:"week",seconds:86400*7,formatter:function(d){return self.formatDate(d)}},{name:"day",seconds:86400,formatter:function(d){return d.getDate()}},{name:"6 hour",seconds:3600*6,formatter:function(d){return self.formatTime(d)}},{name:"hour",seconds:3600,formatter:function(d){return self.formatTime(d)}},{name:"15 minute",seconds:60*15,formatter:function(d){return self.formatTime(d)}},{name:"minute",seconds:60,formatter:function(d){return d.getMinutes()}},{name:"15 second",seconds:15,formatter:function(d){return d.getSeconds()+"s"}},{name:"second",seconds:1,formatter:function(d){return d.getSeconds()+"s"}}];this.unit=function(unitName){return this.units.filter(function(unit){return unitName==unit.name}).shift()};this.formatDate=function(d){return d3.time.format("%b %e")(d)};this.formatTime=function(d){return d.toString().match(/(\d+:\d+):/)[1]};this.ceil=function(time,unit){var nearFuture;var rounded;if(unit.name=="day"){nearFuture=new Date((time+unit.seconds-1)*1e3);rounded=new Date(0);rounded.setMilliseconds(0);rounded.setSeconds(0);rounded.setMinutes(0);rounded.setHours(0);rounded.setDate(nearFuture.getDate());rounded.setMonth(nearFuture.getMonth());rounded.setFullYear(nearFuture.getFullYear());return rounded.getTime()/1e3}if(unit.name=="month"){nearFuture=new Date((time+unit.seconds-1)*1e3);rounded=new Date(0);rounded.setMilliseconds(0);rounded.setSeconds(0);rounded.setMinutes(0);rounded.setHours(0);rounded.setDate(1);rounded.setMonth(nearFuture.getMonth());rounded.setFullYear(nearFuture.getFullYear());return rounded.getTime()/1e3}if(unit.name=="year"){nearFuture=new Date((time+unit.seconds-1)*1e3);rounded=new Date(0);rounded.setFullYear(nearFuture.getFullYear());rounded.setMilliseconds(0);rounded.setSeconds(0);rounded.setMinutes(0);rounded.setHours(0);rounded.setDate(1);rounded.setMonth(0);return rounded.getTime()/1e3}return Math.ceil(time/unit.seconds)*unit.seconds}};Rickshaw.namespace("Rickshaw.Fixtures.Number");Rickshaw.Fixtures.Number.formatKMBT=function(y){var abs_y=Math.abs(y);if(abs_y>=1e12){return y/1e12+"T"}else if(abs_y>=1e9){return y/1e9+"B"}else if(abs_y>=1e6){return y/1e6+"M"}else if(abs_y>=1e3){return y/1e3+"K"}else if(abs_y<1&&y>0){return y.toFixed(2)}else if(abs_y===0){return""}else{return y}};Rickshaw.Fixtures.Number.formatBase1024KMGTP=function(y){var abs_y=Math.abs(y);if(abs_y>=0x4000000000000){return y/0x4000000000000+"P"}else if(abs_y>=1099511627776){return y/1099511627776+"T"}else if(abs_y>=1073741824){return y/1073741824+"G"}else if(abs_y>=1048576){return y/1048576+"M"}else if(abs_y>=1024){return y/1024+"K"}else if(abs_y<1&&y>0){return y.toFixed(2)}else if(abs_y===0){return""}else{return y}};Rickshaw.namespace("Rickshaw.Color.Palette");Rickshaw.Color.Palette=function(args){var color=new Rickshaw.Fixtures.Color;args=args||{};this.schemes={};this.scheme=color.schemes[args.scheme]||args.scheme||color.schemes.colorwheel;this.runningIndex=0;this.generatorIndex=0;if(args.interpolatedStopCount){var schemeCount=this.scheme.length-1;var i,j,scheme=[];for(i=0;iself.graph.x.range()[1]){if(annotation.element){annotation.line.classList.add("offscreen");annotation.element.style.display="none"}annotation.boxes.forEach(function(box){if(box.rangeElement)box.rangeElement.classList.add("offscreen")});return}if(!annotation.element){var element=annotation.element=document.createElement("div");element.classList.add("annotation");this.elements.timeline.appendChild(element);element.addEventListener("click",function(e){element.classList.toggle("active");annotation.line.classList.toggle("active");annotation.boxes.forEach(function(box){if(box.rangeElement)box.rangeElement.classList.toggle("active")})},false)}annotation.element.style.left=left+"px";annotation.element.style.display="block";annotation.boxes.forEach(function(box){var element=box.element;if(!element){element=box.element=document.createElement("div");element.classList.add("content");element.innerHTML=box.content;annotation.element.appendChild(element);annotation.line=document.createElement("div");annotation.line.classList.add("annotation_line");self.graph.element.appendChild(annotation.line);if(box.end){box.rangeElement=document.createElement("div");box.rangeElement.classList.add("annotation_range");self.graph.element.appendChild(box.rangeElement)}}if(box.end){var annotationRangeStart=left;var annotationRangeEnd=Math.min(self.graph.x(box.end),self.graph.x.range()[1]);if(annotationRangeStart>annotationRangeEnd){annotationRangeEnd=left;annotationRangeStart=Math.max(self.graph.x(box.end),self.graph.x.range()[0])}var annotationRangeWidth=annotationRangeEnd-annotationRangeStart;box.rangeElement.style.left=annotationRangeStart+"px";box.rangeElement.style.width=annotationRangeWidth+"px";box.rangeElement.classList.remove("offscreen")}annotation.line.classList.remove("offscreen");annotation.line.style.left=left+"px"})},this)};this.graph.onUpdate(function(){self.update()})};Rickshaw.namespace("Rickshaw.Graph.Axis.Time");Rickshaw.Graph.Axis.Time=function(args){var self=this;this.graph=args.graph;this.elements=[];this.ticksTreatment=args.ticksTreatment||"plain";this.fixedTimeUnit=args.timeUnit;var time=args.timeFixture||new Rickshaw.Fixtures.Time;this.appropriateTimeUnit=function(){var unit;var units=time.units;var domain=this.graph.x.domain();var rangeSeconds=domain[1]-domain[0];units.forEach(function(u){if(Math.floor(rangeSeconds/u.seconds)>=2){unit=unit||u}});return unit||time.units[time.units.length-1]};this.tickOffsets=function(){var domain=this.graph.x.domain();var unit=this.fixedTimeUnit||this.appropriateTimeUnit();var count=Math.ceil((domain[1]-domain[0])/unit.seconds);var runningTick=domain[0];var offsets=[];for(var i=0;iself.graph.x.range()[1])return;var element=document.createElement("div");element.style.left=self.graph.x(o.value)+"px";element.classList.add("x_tick");element.classList.add(self.ticksTreatment);var title=document.createElement("div");title.classList.add("title");title.innerHTML=o.unit.formatter(new Date(o.value*1e3));element.appendChild(title);self.graph.element.appendChild(element);self.elements.push(element)})};this.graph.onUpdate(function(){self.render()})};Rickshaw.namespace("Rickshaw.Graph.Axis.X");Rickshaw.Graph.Axis.X=function(args){var self=this;var berthRate=.1;this.initialize=function(args){this.graph=args.graph;this.orientation=args.orientation||"top";this.pixelsPerTick=args.pixelsPerTick||75;if(args.ticks)this.staticTicks=args.ticks;if(args.tickValues)this.tickValues=args.tickValues;this.tickSize=args.tickSize||4;this.ticksTreatment=args.ticksTreatment||"plain";if(args.element){this.element=args.element;this._discoverSize(args.element,args);this.vis=d3.select(args.element).append("svg:svg").attr("height",this.height).attr("width",this.width).attr("class","rickshaw_graph x_axis_d3");this.element=this.vis[0][0];this.element.style.position="relative";this.setSize({width:args.width,height:args.height})}else{this.vis=this.graph.vis}this.graph.onUpdate(function(){self.render()})};this.setSize=function(args){args=args||{};if(!this.element)return;this._discoverSize(this.element.parentNode,args);this.vis.attr("height",this.height).attr("width",this.width*(1+berthRate));var berth=Math.floor(this.width*berthRate/2);this.element.style.left=-1*berth+"px"};this.render=function(){if(this.graph.width!==this._renderWidth)this.setSize({auto:true});var axis=d3.svg.axis().scale(this.graph.x).orient(this.orientation);axis.tickFormat(args.tickFormat||function(x){return x});if(this.tickValues)axis.tickValues(this.tickValues);this.ticks=this.staticTicks||Math.floor(this.graph.width/this.pixelsPerTick);var berth=Math.floor(this.width*berthRate/2)||0;var transform;if(this.orientation=="top"){var yOffset=this.height||this.graph.height;transform="translate("+berth+","+yOffset+")"}else{transform="translate("+berth+", 0)"}if(this.element){this.vis.selectAll("*").remove()}this.vis.append("svg:g").attr("class",["x_ticks_d3",this.ticksTreatment].join(" ")).attr("transform",transform).call(axis.ticks(this.ticks).tickSubdivide(0).tickSize(this.tickSize));var gridSize=(this.orientation=="bottom"?1:-1)*this.graph.height;this.graph.vis.append("svg:g").attr("class","x_grid_d3").call(axis.ticks(this.ticks).tickSubdivide(0).tickSize(gridSize));this._renderHeight=this.graph.height};this._discoverSize=function(element,args){if(typeof window!=="undefined"){var style=window.getComputedStyle(element,null);var elementHeight=parseInt(style.getPropertyValue("height"),10);if(!args.auto){var elementWidth=parseInt(style.getPropertyValue("width"),10)}}this.width=(args.width||elementWidth||this.graph.width)*(1+berthRate);this.height=args.height||elementHeight||40};this.initialize(args)};Rickshaw.namespace("Rickshaw.Graph.Axis.Y");Rickshaw.Graph.Axis.Y=Rickshaw.Class.create({initialize:function(args){this.graph=args.graph;this.orientation=args.orientation||"right";this.pixelsPerTick=args.pixelsPerTick||75;if(args.ticks)this.staticTicks=args.ticks;if(args.tickValues)this.tickValues=args.tickValues;this.tickSize=args.tickSize||4;this.ticksTreatment=args.ticksTreatment||"plain";this.tickFormat=args.tickFormat||function(y){return y};this.berthRate=.1;if(args.element){this.element=args.element;this.vis=d3.select(args.element).append("svg:svg").attr("class","rickshaw_graph y_axis");this.element=this.vis[0][0];this.element.style.position="relative";this.setSize({width:args.width,height:args.height})}else{this.vis=this.graph.vis}var self=this;this.graph.onUpdate(function(){self.render()})},setSize:function(args){args=args||{};if(!this.element)return;if(typeof window!=="undefined"){var style=window.getComputedStyle(this.element.parentNode,null);var elementWidth=parseInt(style.getPropertyValue("width"),10);if(!args.auto){var elementHeight=parseInt(style.getPropertyValue("height"),10)}}this.width=args.width||elementWidth||this.graph.width*this.berthRate;this.height=args.height||elementHeight||this.graph.height;this.vis.attr("width",this.width).attr("height",this.height*(1+this.berthRate));var berth=this.height*this.berthRate;if(this.orientation=="left"){this.element.style.top=-1*berth+"px"}},render:function(){if(this.graph.height!==this._renderHeight)this.setSize({auto:true});this.ticks=this.staticTicks||Math.floor(this.graph.height/this.pixelsPerTick);var axis=this._drawAxis(this.graph.y);this._drawGrid(axis);this._renderHeight=this.graph.height},_drawAxis:function(scale){var axis=d3.svg.axis().scale(scale).orient(this.orientation);axis.tickFormat(this.tickFormat);if(this.tickValues)axis.tickValues(this.tickValues);if(this.orientation=="left"){var berth=this.height*this.berthRate;var transform="translate("+this.width+", "+berth+")"}if(this.element){this.vis.selectAll("*").remove()}this.vis.append("svg:g").attr("class",["y_ticks",this.ticksTreatment].join(" ")).attr("transform",transform).call(axis.ticks(this.ticks).tickSubdivide(0).tickSize(this.tickSize));return axis},_drawGrid:function(axis){var gridSize=(this.orientation=="right"?1:-1)*this.graph.width;this.graph.vis.append("svg:g").attr("class","y_grid").call(axis.ticks(this.ticks).tickSubdivide(0).tickSize(gridSize))}});Rickshaw.namespace("Rickshaw.Graph.Axis.Y.Scaled");Rickshaw.Graph.Axis.Y.Scaled=Rickshaw.Class.create(Rickshaw.Graph.Axis.Y,{initialize:function($super,args){if(typeof args.scale==="undefined"){throw new Error("Scaled requires scale")}this.scale=args.scale;if(typeof args.grid==="undefined"){this.grid=true}else{this.grid=args.grid}$super(args)},_drawAxis:function($super,scale){var adjustedScale=this.scale.copy().range(scale.range());return $super(adjustedScale)},_drawGrid:function($super,axis){if(this.grid){$super(axis)}}});Rickshaw.namespace("Rickshaw.Graph.Behavior.Series.Highlight");Rickshaw.Graph.Behavior.Series.Highlight=function(args){this.graph=args.graph;this.legend=args.legend;var self=this;var colorSafe={};var activeLine=null;var disabledColor=args.disabledColor||function(seriesColor){return d3.interpolateRgb(seriesColor,d3.rgb("#d8d8d8"))(.8).toString()};this.addHighlightEvents=function(l){l.element.addEventListener("mouseover",function(e){if(activeLine)return;else activeLine=l;self.legend.lines.forEach(function(line,index){if(l===line){if(index>0&&self.graph.renderer.unstack&&(line.series.renderer?line.series.renderer.unstack:true)){var seriesIndex=self.graph.series.length-index-1; +line.originalIndex=seriesIndex;var series=self.graph.series.splice(seriesIndex,1)[0];self.graph.series.push(series)}return}colorSafe[line.series.name]=colorSafe[line.series.name]||line.series.color;line.series.color=disabledColor(line.series.color)});self.graph.update()},false);l.element.addEventListener("mouseout",function(e){if(!activeLine)return;else activeLine=null;self.legend.lines.forEach(function(line){if(l===line&&line.hasOwnProperty("originalIndex")){var series=self.graph.series.pop();self.graph.series.splice(line.originalIndex,0,series);delete line.originalIndex}if(colorSafe[line.series.name]){line.series.color=colorSafe[line.series.name]}});self.graph.update()},false)};if(this.legend){this.legend.lines.forEach(function(l){self.addHighlightEvents(l)})}};Rickshaw.namespace("Rickshaw.Graph.Behavior.Series.Order");Rickshaw.Graph.Behavior.Series.Order=function(args){this.graph=args.graph;this.legend=args.legend;var self=this;if(typeof window.$=="undefined"){throw"couldn't find jQuery at window.$"}if(typeof window.$.ui=="undefined"){throw"couldn't find jQuery UI at window.$.ui"}$(function(){$(self.legend.list).sortable({containment:"parent",tolerance:"pointer",update:function(event,ui){var series=[];$(self.legend.list).find("li").each(function(index,item){if(!item.series)return;series.push(item.series)});for(var i=self.graph.series.length-1;i>=0;i--){self.graph.series[i]=series.shift()}self.graph.update()}});$(self.legend.list).disableSelection()});this.graph.onUpdate(function(){var h=window.getComputedStyle(self.legend.element).height;self.legend.element.style.height=h})};Rickshaw.namespace("Rickshaw.Graph.Behavior.Series.Toggle");Rickshaw.Graph.Behavior.Series.Toggle=function(args){this.graph=args.graph;this.legend=args.legend;var self=this;this.addAnchor=function(line){var anchor=document.createElement("a");anchor.innerHTML="✔";anchor.classList.add("action");line.element.insertBefore(anchor,line.element.firstChild);anchor.onclick=function(e){if(line.series.disabled){line.series.enable();line.element.classList.remove("disabled")}else{if(this.graph.series.filter(function(s){return!s.disabled}).length<=1)return;line.series.disable();line.element.classList.add("disabled")}}.bind(this);var label=line.element.getElementsByTagName("span")[0];label.onclick=function(e){var disableAllOtherLines=line.series.disabled;if(!disableAllOtherLines){for(var i=0;idomainX){dataIndex=Math.abs(domainX-data[i].x)yMax)yMax=y});if(series[0].xxMax)xMax=series[series.length-1].x});xMin-=(xMax-xMin)*this.padding.left;xMax+=(xMax-xMin)*this.padding.right;yMin=this.graph.min==="auto"?yMin:this.graph.min||0;yMax=this.graph.max===undefined?yMax:this.graph.max;if(this.graph.min==="auto"||yMin<0){yMin-=(yMax-yMin)*this.padding.bottom}if(this.graph.max===undefined){yMax+=(yMax-yMin)*this.padding.top}return{x:[xMin,xMax],y:[yMin,yMax]}},render:function(args){args=args||{};var graph=this.graph;var series=args.series||graph.series;var vis=args.vis||graph.vis;vis.selectAll("*").remove();var data=series.filter(function(s){return!s.disabled}).map(function(s){return s.stack});var nodes=vis.selectAll("path").data(data).enter().append("svg:path").attr("d",this.seriesPathFactory());var i=0;series.forEach(function(series){if(series.disabled)return;series.path=nodes[0][i++];this._styleSeries(series)},this)},_styleSeries:function(series){var fill=this.fill?series.color:"none";var stroke=this.stroke?series.color:"none";series.path.setAttribute("fill",fill);series.path.setAttribute("stroke",stroke);series.path.setAttribute("stroke-width",this.strokeWidth);series.path.setAttribute("class",series.className)},configure:function(args){args=args||{};Rickshaw.keys(this.defaults()).forEach(function(key){if(!args.hasOwnProperty(key)){this[key]=this[key]||this.graph[key]||this.defaults()[key];return}if(typeof this.defaults()[key]=="object"){Rickshaw.keys(this.defaults()[key]).forEach(function(k){this[key][k]=args[key][k]!==undefined?args[key][k]:this[key][k]!==undefined?this[key][k]:this.defaults()[key][k]},this)}else{this[key]=args[key]!==undefined?args[key]:this[key]!==undefined?this[key]:this.graph[key]!==undefined?this.graph[key]:this.defaults()[key]}},this)},setStrokeWidth:function(strokeWidth){if(strokeWidth!==undefined){this.strokeWidth=strokeWidth}},setTension:function(tension){if(tension!==undefined){this.tension=tension}}});Rickshaw.namespace("Rickshaw.Graph.Renderer.Line");Rickshaw.Graph.Renderer.Line=Rickshaw.Class.create(Rickshaw.Graph.Renderer,{name:"line",defaults:function($super){return Rickshaw.extend($super(),{unstack:true,fill:false,stroke:true})},seriesPathFactory:function(){var graph=this.graph;var factory=d3.svg.line().x(function(d){return graph.x(d.x)}).y(function(d){return graph.y(d.y)}).interpolate(this.graph.interpolation).tension(this.tension);factory.defined&&factory.defined(function(d){return d.y!==null});return factory}});Rickshaw.namespace("Rickshaw.Graph.Renderer.Stack");Rickshaw.Graph.Renderer.Stack=Rickshaw.Class.create(Rickshaw.Graph.Renderer,{name:"stack",defaults:function($super){return Rickshaw.extend($super(),{fill:true,stroke:false,unstack:false})},seriesPathFactory:function(){var graph=this.graph;var factory=d3.svg.area().x(function(d){return graph.x(d.x)}).y0(function(d){return graph.y(d.y0)}).y1(function(d){return graph.y(d.y+d.y0)}).interpolate(this.graph.interpolation).tension(this.tension);factory.defined&&factory.defined(function(d){return d.y!==null});return factory}});Rickshaw.namespace("Rickshaw.Graph.Renderer.Bar");Rickshaw.Graph.Renderer.Bar=Rickshaw.Class.create(Rickshaw.Graph.Renderer,{name:"bar",defaults:function($super){var defaults=Rickshaw.extend($super(),{gapSize:.05,unstack:false});delete defaults.tension;return defaults},initialize:function($super,args){args=args||{};this.gapSize=args.gapSize||this.gapSize;$super(args)},domain:function($super){var domain=$super();var frequentInterval=this._frequentInterval(this.graph.stackedData.slice(-1).shift());domain.x[1]+=Number(frequentInterval.magnitude);return domain},barWidth:function(series){var frequentInterval=this._frequentInterval(series.stack);var barWidth=this.graph.x(series.stack[0].x+frequentInterval.magnitude*(1-this.gapSize));return barWidth},render:function(args){args=args||{};var graph=this.graph;var series=args.series||graph.series;var vis=args.vis||graph.vis;vis.selectAll("*").remove();var barWidth=this.barWidth(series.active()[0]);var barXOffset=0;var activeSeriesCount=series.filter(function(s){return!s.disabled}).length;var seriesBarWidth=this.unstack?barWidth/activeSeriesCount:barWidth;var transform=function(d){var matrix=[1,0,0,d.y<0?-1:1,0,d.y<0?graph.y.magnitude(Math.abs(d.y))*2:0];return"matrix("+matrix.join(",")+")"};series.forEach(function(series){if(series.disabled)return;var barWidth=this.barWidth(series);var nodes=vis.selectAll("path").data(series.stack.filter(function(d){return d.y!==null})).enter().append("svg:rect").attr("x",function(d){return graph.x(d.x)+barXOffset}).attr("y",function(d){return graph.y(d.y0+Math.abs(d.y))*(d.y<0?-1:1)}).attr("width",seriesBarWidth).attr("height",function(d){return graph.y.magnitude(Math.abs(d.y))}).attr("transform",transform);Array.prototype.forEach.call(nodes[0],function(n){n.setAttribute("fill",series.color)});if(this.unstack)barXOffset+=seriesBarWidth},this)},_frequentInterval:function(data){var intervalCounts={};for(var i=0;i0){this[0].data.forEach(function(plot){item.data.push({x:plot.x,y:0})})}else if(item.data.length===0){item.data.push({x:this.timeBase-(this.timeInterval||0),y:0})}this.push(item);if(this.legend){this.legend.addLine(this.itemByName(item.name))}},addData:function(data,x){var index=this.getIndex();Rickshaw.keys(data).forEach(function(name){if(!this.itemByName(name)){this.addItem({name:name})}},this);this.forEach(function(item){item.data.push({x:x||(index*this.timeInterval||1)+this.timeBase,y:data[item.name]||0})},this)},getIndex:function(){return this[0]&&this[0].data&&this[0].data.length?this[0].data.length:0},itemByName:function(name){for(var i=0;i1;i--){this.currentSize+=1;this.currentIndex+=1;this.forEach(function(item){item.data.unshift({x:((i-1)*this.timeInterval||1)+this.timeBase,y:0,i:i})},this)}}},addData:function($super,data,x){$super(data,x);this.currentSize+=1;this.currentIndex+=1;if(this.maxDataPoints!==undefined){while(this.currentSize>this.maxDataPoints){this.dropData()}}},dropData:function(){this.forEach(function(item){item.data.splice(0,1)});this.currentSize-=1},getIndex:function(){return this.currentIndex}}); \ No newline at end of file diff --git a/cat-home/src/main/webapp/jsp/system/abtest/abtest.jsp b/cat-home/src/main/webapp/jsp/system/abtest/abtest.jsp deleted file mode 100644 index 6f9fa40369ab7c2fc3bcc4cdbb31fc67726b9c98..0000000000000000000000000000000000000000 --- a/cat-home/src/main/webapp/jsp/system/abtest/abtest.jsp +++ /dev/null @@ -1,5 +0,0 @@ -<%@ page contentType="text/html; charset=utf-8" %> - - - -View of abtest page under system \ No newline at end of file diff --git a/cat-home/src/main/webapp/jsp/system/abtest/abtestAjax.jsp b/cat-home/src/main/webapp/jsp/system/abtest/abtestAjax.jsp new file mode 100644 index 0000000000000000000000000000000000000000..344848cfc4e3a38a57ac973c13890356f78c7a63 --- /dev/null +++ b/cat-home/src/main/webapp/jsp/system/abtest/abtestAjax.jsp @@ -0,0 +1 @@ +${ctx.responseJson} diff --git a/cat-home/src/main/webapp/jsp/system/abtest/abtestAllTest.jsp b/cat-home/src/main/webapp/jsp/system/abtest/abtestAllTest.jsp index 347f8231c6cfdb54e7ae3c669fc3aae249bf3c07..1052022b6a76d7f991dfabf6453844d1d403911b 100644 --- a/cat-home/src/main/webapp/jsp/system/abtest/abtestAllTest.jsp +++ b/cat-home/src/main/webapp/jsp/system/abtest/abtestAllTest.jsp @@ -9,73 +9,66 @@ - - - - - - - - - - - - + + + + +
@@ -153,12 +150,12 @@ tr.centerth>th {
- + - + @@ -219,16 +216,16 @@ tr.centerth>th { + begin="${(beginPage * 5 + 1)<= model.listViewModel.totalPages ? (beginPage * 5 + 1) : model.listViewModel.totalPages }" + end="${(beginPage + 1) * 5 <= model.listViewModel.totalPages ? (beginPage + 1) * 5 : model.listViewModel.totalPages}">
  • ${pageNum}
  • - -
  • = model.totalPages ? ' class="disabled"' : ''}>Last
  • +
  • = model.listViewModel.totalPages ? ' class="disabled"' : ''}>Last
  • diff --git a/cat-home/src/main/webapp/jsp/system/abtest/abtestCaculator.jsp b/cat-home/src/main/webapp/jsp/system/abtest/abtestCaculator.jsp new file mode 100644 index 0000000000000000000000000000000000000000..0b9188b9835084c34b86ced4dfe3d5fea018967d --- /dev/null +++ b/cat-home/src/main/webapp/jsp/system/abtest/abtestCaculator.jsp @@ -0,0 +1,96 @@ +<%@ page contentType="text/html; charset=utf-8"%> +<%@ taglib prefix="a" uri="/WEB-INF/app.tld"%> +<%@ taglib prefix="w" uri="http://www.unidal.org/web/core"%> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> +<%@ taglib prefix="res" uri="http://www.unidal.org/webres"%> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%> + + + + + + + +
    +
    +

    A/B Test Caculator

    +
    +
    +
    +
    +
    + +
    + % +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    + +
    + +
    ANDOR
    + + + + + + + + + + + + + + + + + + + + + + + + +
    "A" Conversion Rate"B" Conversion RateDifferenceSize Per GroupTotal ParticipantsConfidence LevelDays
    ${item.difference }${item.sizePerGroup }${item.totalParticipants }${item.confidenceInterval }${item.days }
    + + + + + \ No newline at end of file diff --git a/cat-home/src/main/webapp/jsp/system/abtest/abtestCreate.jsp b/cat-home/src/main/webapp/jsp/system/abtest/abtestCreate.jsp index 6e95abf719127a153976e7ee4e825ff478b8de86..28501a64759612f38641110b62f275b4215c50cd 100644 --- a/cat-home/src/main/webapp/jsp/system/abtest/abtestCreate.jsp +++ b/cat-home/src/main/webapp/jsp/system/abtest/abtestCreate.jsp @@ -6,229 +6,396 @@ + + - - - - - - - - - - - - - -
    -

    Create ABTest

    - - -
    -
    - - ${ctx.exception.message} -
    -
    -
    - -
    -
    - Created! Going to the list page after - seconds ... - - -
    -
    -
    -
    -
    -
    - Cancel - -
    Basic Information
    -
    -
    - -
    - -
    -
    -
    - -
    - -
    -
    -
    - -
    - -
    -
    -
    - -
    -
    - - -
    -
    -
    -
    - -
    -
    - - -
    -
    -
    -
    - -
    - -
    -
    -
    Group Strategy
    -
    -
    - -
    - -
    -
    -
    - -
    - -
    -
    -
    -
    -
    - - - - + - - - function submit0(form) { - $("#successMsg").html(''); - $("#errorMsg").html(''); - $.ajax({ - type : $(form).attr('method'), - url : $(form).attr('action'), - data : $(form).serialize(), - dataType : "json", - success : submitDone, - error : abtest.httpError - }); - return false; + + } + }); + + $("#edit1,#save1,#cancel1").click(function(e){ + var parent = $(this).parent(); + $("span",parent).toggleClass("hide"); + $("input",parent).toggleClass("hide"); + $("a",parent).toggleClass("hide"); + + var id = $(this).attr("id"); + + if(id == "save1"){ + if($("input",parent).val() != ""){ + $("span",parent).text($("input",parent).val()); + }else{ + $("span",parent).text("No URL excluded"); + } + }else if(id == "edit1"){ + if($("span",parent).text() != "No URL excluded"){ + $("input",parent).val($("span",parent).text()); + } + } + }); + + $("#edit2,#save2").click(function(e){ + var parent = $(this).parent(); + $("span",parent).toggleClass("hide"); + $("a",parent).toggleClass("hide"); + $("div:first",parent).toggleClass("hide"); + + var id = $(this).attr("id"); + + if(id == "save2"){ + var conditions = getConditionsHTML(); + + if(conditions){ + $("span",parent).html(conditions); + }else{ + $("span",parent).html("All visitors"); + } + }else if(id == "edit2"){ + } + }); + + $("#edit3,#save3,#cancel3").click(function(e){ + var parent = $(this).parent(); + $("span:first",parent).toggleClass("hide"); + $("a",parent).toggleClass("hide"); + $('.slider').toggleClass('hide'); + }); + + $(function() { + $('#datetimepicker1').datetimepicker(); + $('#datetimepicker2').datetimepicker(); + //domain selector + $("#domains").select2({ + placeholder : "select which domains to run this ab test", + allowClear : true + }); + + var showData = function(){ + $(".add-on:eq(3)").text(data.getValue()); + } + var data = $('.slider').slider().on('slide',showData).data('slider'); + + $('.slider').toggleClass("hide"); + + $("#domains").val(initDomains).trigger("change"); + + + $('#strategyId option[value=${item.id}]').data(JSON.parse('${item.descriptor}')); + + + //tips + $('i[tips]').popover(); + //validate + $('#form').validation(); + $('#groupStrategyFrom').validation(); + }); +
    \ No newline at end of file diff --git a/cat-home/src/main/webapp/jsp/system/abtest/abtestDetail.jsp b/cat-home/src/main/webapp/jsp/system/abtest/abtestDetail.jsp index dc9515d74c5eb4ed73889123ed77e7f964a8874d..ba31eb1c7eefdb25146801e932d0d30757e2eee4 100644 --- a/cat-home/src/main/webapp/jsp/system/abtest/abtestDetail.jsp +++ b/cat-home/src/main/webapp/jsp/system/abtest/abtestDetail.jsp @@ -21,21 +21,21 @@ div.controls input { background-color: #F7F7F9; cursor: text; } + +.inline-space{ + margin-bottom: 5px; +} + - - - - + - - - +

    @@ -43,9 +43,9 @@ div.controls input {