diff --git a/modules/distribution/pom.xml b/modules/distribution/pom.xml index 78b8d4a4..07187467 100644 --- a/modules/distribution/pom.xml +++ b/modules/distribution/pom.xml @@ -96,6 +96,73 @@ org.apache.maven.plugins maven-antrun-plugin + + + + 2-identity-h2-table-gen + package + + + + + + + + + + + + + + + + + + + + + + + run + + + + + + + social-h2-table-gen + package + + + + + + + + + + + + + + run + + + + create-device-mgt-schema @@ -124,33 +191,33 @@ - - - create-idp-mgt-schema - package - - run - - - - - - - + + + + + + + + + + + + + + + - - - - - - - - - + + + + + + + + + + + create-api-mgt-schema @@ -179,175 +246,175 @@ - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - - - - - - - - - + + + + + + + + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - - - - - - - - - + + + + + + + + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - - - - - - - - - + + + + + + + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - - - - + + + + + + + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - - - - + + + + + + + + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - - - - - - - - - + + + + + + + + + + @@ -457,6 +524,12 @@ + + + + + **/version.txt - **/master-datasources.xml + **/carbon.xml **/registry.xml **/client-truststore.jks **/wso2carbon.jks **/WSO2AM_DB.h2.db - **/WSO2DM_DB.h2.db **/conf/axis2/axis2_client.xml **/conf/etc/launch.ini + **/client-truststore.jks + **/wso2carbon.jks + + + **/registry.xml + **/axis2/axis2.xml + **/org.wso2.carbon.identity.relyingparty.ui-3.1.0.SNAPSHOT.jar + **/org.wso2.carbon.identity.user.registration.ui-3.1.0.SNAPSHOT.jar + + **/log4j.properties + **/email/email-admin-config.xml + **/claim-config.xml + **/ciphertool.bat + **/ciphertool.sh + **/cipher-text.properties + **/cipher-tool.properties + **/lib/org.wso2.ciphertool-1.0.0-wso2v3.jar @@ -63,9 +79,15 @@ **/stratos.xml **/multitenancy-packages.xml **/usage-throttling-agent-config.xml - **/cloud-services-desc.xml + + + - true + + + **/tenant-mgt.xml + + @@ -76,6 +98,15 @@ **/tenant-mgt.xml + + + + + ../p2-profile-gen/target/wso2carbon-core-${carbon.kernel.version}/repository/resources + + ${pom.artifactId}-${pom.version}/repository/resources + + src/repository/resources/styles/css ${pom.artifactId}-${pom.version}/resources/allthemes/Default/admin @@ -113,11 +144,7 @@ ${pom.artifactId}-${pom.version}/modules/ - **/carbon/ - **/jaggery-test/ - **/process/ - **/uuid/ - **/i18n/ + */** @@ -125,8 +152,7 @@ ${pom.artifactId}-${pom.version}/repository/conf **/api-manager.xml - **/security/ - + **/sso-idp-config.xml @@ -137,6 +163,11 @@ **/entitlement.properties **/trusted-idp-config.xml + + **/cipher-text.properties + **/cipher-tool.properties + **/cipher-standalone-config.properties + @@ -148,15 +179,16 @@ **/** - - src/repository/conf/datasources - ${pom.artifactId}-${pom.version}/repository/conf/datasources - - - **/master-datasources.xml - **/cdm-datasources.xml - - + + + + + + + + + + src/repository/resources ${pom.artifactId}-${pom.version}/repository/resources @@ -270,6 +302,165 @@ + + + + + + + ../p2-profile-gen/target/wso2carbon-core-${carbon.kernel.version}/repository/deployment/server/jaggeryapps + + ${pom.artifactId}-${pom.version}/repository/deployment/server/jaggeryapps/ + + + */** + + + + + + + lib/home + + ${pom.artifactId}-${pom.version}/repository/deployment/server/webapps/STRATOS_ROOT + + + + + + ../p2-profile-gen/target/wso2carbon-core-${carbon.kernel.version}/client + ${pom.artifactId}-${pom.version}/lib + + */** + + + + + + + + ../p2-profile-gen/target/wso2carbon-core-${carbon.kernel.version}/repository/deployment/server/jaxwebapps + + ${pom.artifactId}-${pom.version}/repository/deployment/server/jaxwebapps + + + oauth2endpoints.war + + + + + + target/jaxwebapps + ${pom.artifactId}-${pom.version}/repository/deployment/server/jaxwebapps + + + **/** + + + + + + ../p2-profile-gen/target/wso2carbon-core-${carbon.kernel.version}/bin + ${pom.artifactId}-${pom.version}/bin/ + + **/ciphertool.sh + **/ciphertool.bat + + + + + + ../p2-profile-gen/target/wso2carbon-core-${carbon.kernel.version}/lib + ${pom.artifactId}-${pom.version}/lib/ + + **/org.wso2.ciphertool-1.0.0-wso2v3.jar + + + + + + src/repository/conf/etc + ${pom.artifactId}-${pom.version}/repository/conf/etc/ + + **/** + + + + + + + src/repository/conf + ${pom.artifactId}-${pom.version}/repository/conf/identity/ + + **/sso-idp-config.xml + + + + + + src/repository/conf/identity + ${pom.artifactId}-${pom.version}/repository/conf/identity/ + + **/** + + + + + + src/repository/conf/bam + ${pom.artifactId}-${pom.version}/repository/conf/bam + + **/es-bam.xml + + + + + + + ../p2-profile-gen/target/wso2carbon-core-${carbon.kernel.version}/repository/resources/security/ + + ${pom.artifactId}-${pom.version}/repository/resources/security/ + + + **/sso_redirect.html + + + + + + + ../p2-profile-gen/target/wso2carbon-core-${carbon.kernel.version}/repository/deployment/server/webapps + + ${pom.artifactId}-${pom.version}/repository/deployment/server/webapps + + + shindig.war + + + + + + src/repository/bam + ${pom.artifactId}-${pom.version}/repository/bam/ + + **/** + + + + + + src/repository/conf/security + ${pom.artifactId}-${pom.version}/repository/conf/identity/ + + **/application-authenticators.xml + + **/application-authentication.xml + + + + + + @@ -576,15 +767,16 @@ 644 + - - - ../p2-profile-gen/target/wso2carbon-core-${carbon.kernel.version}/repository/conf/etc/webapp-authenticator-config.xml - - ${pom.artifactId}-${pom.version}/repository/conf/etc - true - 644 - + + + + + + + + @@ -657,7 +849,7 @@ 644 - + target/wso2carbon-core-${carbon.kernel.version}/repository/conf/carbon.xml ${pom.artifactId}-${pom.version}/repository/conf/ @@ -681,5 +873,125 @@ 644 + + + + + + + ../p2-profile-gen/target/wso2carbon-core-${carbon.kernel.version}/repository/conf/governance.xml + + ${pom.artifactId}-${pom.version}/repository/conf + true + 644 + + + + + src/repository/conf/registry.xml + ${pom.artifactId}-${pom.version}/repository/conf/ + true + + + + + src/repository/conf/multitenancy/cloud-services-desc.xml + ${pom.artifactId}-${pom.version}/repository/conf/multitenancy/ + true + + + + + + ../p2-profile-gen/target/wso2carbon-core-${carbon.kernel.version}/repository/conf/log4j.properties + + ${pom.artifactId}-${pom.version}/repository/conf/ + + + + + + target/wso2carbon-core-${carbon.kernel.version}/repository/conf/axis2/axis2.xml + ${pom.artifactId}-${pom.version}/repository/conf/axis2/ + true + 644 + + + + + ../p2-profile-gen/target/wso2carbon-core-${carbon.kernel.version}/repository/conf/identity/identity-mgt.properties + + ${pom.artifactId}-${pom.version}/repository/conf/identity + true + 644 + + + + + + ../p2-profile-gen/target/wso2carbon-core-${carbon.kernel.version}/repository/conf/email/email-admin-config.xml + + ${pom.artifactId}-${pom.version}/repository/conf/email + true + 644 + + + + + + ../p2-profile-gen/target/wso2carbon-core-${carbon.kernel.version}/repository/conf/claim-config.xml + + ${pom.artifactId}-${pom.version}/repository/conf + true + 644 + + + + + src/repository/database/WSO2IDENTITY_DB.h2.db + ${pom.artifactId}-${pom.version}/repository/database + 644 + + + + + ../p2-profile-gen/target/wso2carbon-core-${carbon.kernel.version}/repository/conf/social.xml + + ${pom.artifactId}-${pom.version}/repository/conf/ + true + 644 + + + + + + ../p2-profile-gen/target/wso2carbon-core-${carbon.kernel.version}/repository/conf/datasources/social-datasources.xml + + ${pom.artifactId}-${pom.version}/repository/conf/datasources/ + true + 644 + + + + + + ../p2-profile-gen/target/wso2carbon-core-${carbon.kernel.version}/repository/conf/association-config.xml + + ${pom.artifactId}-${pom.version}/repository/conf/etc + true + 644 + + + + + + ../p2-profile-gen/target/wso2carbon-core-${carbon.kernel.version}/repository/conf/shindig.properties + ${pom.artifactId}-${pom.version}/repository/conf + true + 644 + + + + diff --git a/modules/distribution/src/repository/conf/api-manager.xml b/modules/distribution/src/repository/conf/api-manager.xml index 74219103..ec166a68 100755 --- a/modules/distribution/src/repository/conf/api-manager.xml +++ b/modules/distribution/src/repository/conf/api-manager.xml @@ -8,36 +8,36 @@ EMM - - - false + + + false - - + + - - + + - - + - + JDBC driver for the database. + --> + X-JWT-Assertion - - + + - - - - - + + - - - - - + + + + + + + + - - - - - Production and Sandbox - - https://${carbon.local.ip}:${mgt.transport.https.port}/services/ - - admin - - admin - - http://${carbon.local.ip}:${mgt.transport.http.port},https://${carbon.local.ip}:${mgt.transport.https.port} - + + + + + Production and Sandbox + + https://${carbon.local.ip}:${mgt.transport.https.port}/services/ + + admin + + admin + + http://${carbon.local.ip}:${http.nio.port},https://${carbon.local.ip}:${https.nio.port} + - + false - - referer + + referer - - false + + false - + @@ -212,23 +212,23 @@ --> - + - - false + + false - - UA-XXXXXXXX-X + + UA-XXXXXXXX-X - + @@ -276,26 +276,26 @@ 10397 10000 10397 - - true - - + + true + + - + - - oauth2/token - - - false + + oauth2/token + + + false @@ -356,27 +356,27 @@ --> false - - - true - - - true - - - + --> + true + + + true + + + @@ -398,27 +398,27 @@ related to those operations --> - - - - - - + + + + + + - + + false + + - - false - - - - - + + + - - - + + + --> - - - true - - https://localhost:9443,http://localhost:9763 + + true - - authorization,Access-Control-Allow-Origin,Content-Type + + https://localhost:9443,http://localhost:9763 - - GET,POST,PUT,DELETE,OPTIONS + + authorization,Access-Control-Allow-Origin,Content-Type + + + GET,POST,PUT,DELETE,OPTIONS diff --git a/modules/distribution/src/repository/conf/datasources/master-datasources.xml b/modules/distribution/src/repository/conf/datasources/master-datasources.xml index d0e734fc..c564318d 100755 --- a/modules/distribution/src/repository/conf/datasources/master-datasources.xml +++ b/modules/distribution/src/repository/conf/datasources/master-datasources.xml @@ -98,132 +98,6 @@ - - FireAlarmDM_DS - The datasource used for Firealarm database - - jdbc/FireAlarmDM_DB - - - - jdbc:h2:repository/database/FireAlarmDM_DB;DB_CLOSE_ON_EXIT=FALSE - wso2carbon - wso2carbon - org.h2.Driver - 50 - 60000 - true - SELECT 1 - 30000 - - - - - - ArduinoDM_DS - The datasource used for Arduino database - - jdbc/ArduinoDM_DB - - - - jdbc:h2:repository/database/ArduinoDM_DB;DB_CLOSE_ON_EXIT=FALSE - wso2carbon - wso2carbon - org.h2.Driver - 50 - 60000 - true - SELECT 1 - 30000 - - - - - - AndroidSenseDM_DB - The datasource used for Android database - - jdbc/AndroidSenseDM_DB - - - - jdbc:h2:repository/database/AndroidSenseDM_DB;DB_CLOSE_ON_EXIT=FALSE - wso2carbon - wso2carbon - org.h2.Driver - 50 - 60000 - true - SELECT 1 - 30000 - - - - - - SensebotDM_DS - The datasource used for Sensebot database - - jdbc/SensebotDM_DB - - - - jdbc:h2:repository/database/SensebotDM_DB;DB_CLOSE_ON_EXIT=FALSE - wso2carbon - wso2carbon - org.h2.Driver - 50 - 60000 - true - SELECT 1 - 30000 - - - - - - DigitalDisplayDM_DS - The datasource used for Digital Display database - - jdbc/DigitalDisplayDM_DB - - - - jdbc:h2:repository/database/DigitalDisplayDM_DB;DB_CLOSE_ON_EXIT=FALSE - wso2carbon - wso2carbon - org.h2.Driver - 50 - 60000 - true - SELECT 1 - 30000 - - - - - - RaspberrypiDM_DS - The datasource used for Raspberrypi database - - jdbc/RaspberrypiDM_DB - - - - jdbc:h2:repository/database/RaspberrypiDM_DB;DB_CLOSE_ON_EXIT=FALSE - wso2carbon - wso2carbon - org.h2.Driver - 50 - 60000 - true - SELECT 1 - 30000 - - - - JAGH2 The datasource used for by the Jaggery Storage Manager diff --git a/modules/distribution/src/repository/conf/emm-config.xml b/modules/distribution/src/repository/conf/emm-config.xml deleted file mode 100644 index 4350f3d2..00000000 --- a/modules/distribution/src/repository/conf/emm-config.xml +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - - 60000 - - - - https://192.168.1.2:9443/emm/scep - https://192.168.1.2:9443/emm/profile - https://192.168.1.2:9443/emm/checkin - https://192.168.1.2:9443/emm/server - - - http://192.168.1.2:9763/emm/api/devices/iostokenregister - - - - ${carbon.home}/repository/resources/security/wso2emm.jks - - JKS - - wso2carbon - - cacert - - cacert - - racert - - racert - - - - diff --git a/modules/distribution/src/repository/conf/identity.xml b/modules/distribution/src/repository/conf/identity.xml index 737af4f6..929d6ea6 100755 --- a/modules/distribution/src/repository/conf/identity.xml +++ b/modules/distribution/src/repository/conf/identity.xml @@ -37,6 +37,13 @@ true + + 120 + 20160 + 20160 + 1140 + + @@ -68,10 +75,42 @@ + + https://localhost:9443/openidserver + https://localhost:9443/openid/ + + false + + 7200 + + false + + false + + 36000 + + false + org.wso2.carbon.identity.provider.openid.claims.DefaultClaimsRetriever + + - https://10.100.5.3:9443/oauth/request-token + -1 + -1 + -1 + -1 + https://localhost:9443/oauth/request-token + https://localhost:9443/oauth/access-token + https://localhost:9443/oauth/authorize-url + + + + + + 300 @@ -80,17 +119,21 @@ 3600 3600 + + 84600 300 true - - - org.wso2.carbon.identity.oauth.tokenprocessor.PlainTextPersistenceProcessor - + + true + + org.wso2.carbon.identity.oauth.tokenprocessor.PlainTextPersistenceProcessor + - org.wso2.carbon.identity.oauth2.token.handlers.clientauth.BasicAuthClientAuthHandler + + false + + org.wso2.carbon.apimgt.keymgt.handlers.ExtendedPasswordGrantHandler @@ -128,10 +173,17 @@ urn:ietf:params:oauth:grant-type:saml2-bearer org.wso2.carbon.identity.oauth2.token.handlers.grant.saml.SAML2BearerGrantHandler + + iwa:ntlm + org.wso2.carbon.identity.oauth2.token.handlers.grant.iwa.ntlm.NTLMAuthenticationGrantHandler + + false + IDN_OAUTH2_ACCESS_TOKEN_A. --> - + false org.wso2.carbon.identity.oauth2.authcontext.JWTTokenGenerator org.wso2.carbon.identity.oauth2.authcontext.DefaultClaimsRetriever @@ -167,21 +219,23 @@ 15 - - - + + + - - + + org.wso2.carbon.identity.openidconnect.DefaultIDTokenBuilder + https://localhost:9443/oauth2endpoints/token + http://wso2.org/claims/givenname + org.wso2.carbon.identity.openidconnect.SAMLAssertionClaimsCallback + 3600 + http://wso2.org/claims + org.wso2.carbon.identity.oauth.endpoint.user.impl.UserInfoUserStoreClaimRetriever + org.wso2.carbon.identity.oauth.endpoint.user.impl.UserInforRequestDefaultValidator + org.wso2.carbon.identity.oauth.endpoint.user.impl.UserInfoISAccessTokenValidator + org.wso2.carbon.identity.oauth.endpoint.user.impl.UserInfoJSONResponseBuilder + false + @@ -198,6 +252,8 @@ + 157680000 + 157680000 localhost https://localhost:9443/samlsso 5 @@ -209,6 +265,7 @@ http://wso2.org/claims false org.wso2.carbon.identity.sso.saml.builders.claims.DefaultClaimsRetriever + org.wso2.carbon.identity.sso.saml.builders.assertion.DefaultSAMLAssertionBuilder org.wso2.carbon.identity.sso.saml.builders.encryption.DefaultSSOEncrypter org.wso2.carbon.identity.sso.saml.builders.signature.DefaultSSOSigner org.wso2.carbon.identity.sso.saml.validators.SAML2HTTPRedirectDeflateSignatureValidator @@ -216,7 +273,7 @@ 5 - false + false @@ -240,9 +297,28 @@ ${carbon.home}/repository/resources/security/wso2carbon.jks wso2carbon + + localhost + + + 5 + + + 10 + local://services + + + + + false - + \ No newline at end of file diff --git a/modules/distribution/src/repository/conf/identity/service-providers/sp_dashboard.xml b/modules/distribution/src/repository/conf/identity/service-providers/sp_dashboard.xml new file mode 100644 index 00000000..c300d67c --- /dev/null +++ b/modules/distribution/src/repository/conf/identity/service-providers/sp_dashboard.xml @@ -0,0 +1,49 @@ + + 2 + wso2_sp_dashboard + Default Service Provider + + + + wso2.my.dashboard + samlsso + + + + + + + + 1 + + + BasicAuthenticator + basicauth + true + + + + true + true + + + + + + + + true + + + diff --git a/modules/distribution/src/repository/conf/security/application-authentication.xml b/modules/distribution/src/repository/conf/security/application-authentication.xml new file mode 100644 index 00000000..9f02a255 --- /dev/null +++ b/modules/distribution/src/repository/conf/security/application-authentication.xml @@ -0,0 +1,123 @@ + + + + + + + + + + jdbc/WSO2CarbonDB + + + + + smart + + + /sso/login + + + + org.wso2.carbon.identity.application.authentication.framework.handler.request.impl.DefaultRequestCoordinator + org.wso2.carbon.identity.application.authentication.framework.handler.request.impl.DefaultAuthenticationRequestHandler + org.wso2.carbon.identity.application.authentication.framework.handler.request.impl.DefaultLogoutRequestHandler + org.wso2.carbon.identity.application.authentication.framework.handler.sequence.impl.DefaultStepBasedSequenceHandler + org.wso2.carbon.identity.application.authentication.framework.handler.sequence.impl.DefaultRequestPathBasedSequenceHandler + org.wso2.carbon.identity.application.authentication.framework.handler.step.impl.DefaultStepHandler + org.wso2.carbon.identity.application.authentication.framework.handler.hrd.impl.DefaultHomeRealmDiscoverer + org.wso2.carbon.identity.application.authentication.framework.handler.claims.impl.DefaultClaimHandler + org.wso2.carbon.identity.application.authentication.framework.handler.provisioning.impl.DefaultProvisioningHandler + + + + + + + + + + + + + + + + + + + + + + + + + + + + /authenticationendpoint/login.do + /repository/resources/security/client-truststore.jks + wso2carbon + + + + + + + + + + + + + + + + org.wso2.carbon.identity.application.mgt.dao.impl.ApplicationDAOImpl + org.wso2.carbon.identity.application.mgt.dao.impl.OAuthApplicationDAOImpl + org.wso2.carbon.identity.application.mgt.dao.impl.SAMLApplicationDAOImpl + org.wso2.carbon.identity.application.mgt.dao.impl.IdentityProviderDAOImpl + http://wso2.org/claims + + + diff --git a/modules/distribution/src/repository/conf/security/application-authenticators.xml b/modules/distribution/src/repository/conf/security/application-authenticators.xml new file mode 100644 index 00000000..c13c0401 --- /dev/null +++ b/modules/distribution/src/repository/conf/security/application-authenticators.xml @@ -0,0 +1,23 @@ + + + + + + + + + \ No newline at end of file diff --git a/modules/distribution/src/repository/conf/sso-idp-config.xml b/modules/distribution/src/repository/conf/sso-idp-config.xml new file mode 100755 index 00000000..99f61be9 --- /dev/null +++ b/modules/distribution/src/repository/conf/sso-idp-config.xml @@ -0,0 +1,52 @@ + + + https://stratos-local.wso2.com/carbon/tenant-register/select_domain.jsp + + + store + https://localhost:9443/store/acs + true + /store/login.jag + + + social + https://localhost:9443/social/acs + true + /social/login + + + publisher + https://localhost:9443/publisher/acs + true + /publisher/controllers/login.jag + + + emm + https://localhost:9443/emm/acs + true + /emm/login + + + mam + https://localhost:9443/mam/acs + true + /mam/login + + + diff --git a/modules/distribution/src/repository/conf/tomcat/context.xml b/modules/distribution/src/repository/conf/tomcat/context.xml index 33db120f..96452af2 100644 --- a/modules/distribution/src/repository/conf/tomcat/context.xml +++ b/modules/distribution/src/repository/conf/tomcat/context.xml @@ -35,4 +35,16 @@ --> + + diff --git a/modules/distribution/src/repository/database/WSO2IDENTITY_DB.h2.db b/modules/distribution/src/repository/database/WSO2IDENTITY_DB.h2.db new file mode 100644 index 00000000..16a8b5c8 Binary files /dev/null and b/modules/distribution/src/repository/database/WSO2IDENTITY_DB.h2.db differ diff --git a/modules/distribution/src/repository/dbscripts/cdm/h2.sql b/modules/distribution/src/repository/dbscripts/cdm/h2.sql new file mode 100644 index 00000000..3ceea15a --- /dev/null +++ b/modules/distribution/src/repository/dbscripts/cdm/h2.sql @@ -0,0 +1,26 @@ +CREATE TABLE IF NOT EXISTS DM_DEVICE_TYPE +( + ID INT(11) auto_increment NOT NULL, + NAME VARCHAR(300) NULL DEFAULT NULL, + PRIMARY KEY (ID) +); + +CREATE TABLE IF NOT EXISTS DM_DEVICE +( + ID INT auto_increment NOT NULL, + DESCRIPTION TEXT NULL DEFAULT NULL, + NAME VARCHAR(100) NULL DEFAULT NULL, + DATE_OF_ENROLLMENT BIGINT NULL DEFAULT NULL, + DATE_OF_LAST_UPDATE BIGINT NULL DEFAULT NULL, + OWNERSHIP VARCHAR(45) NULL DEFAULT NULL, + STATUS VARCHAR(15) NULL DEFAULT NULL, + DEVICE_TYPE_ID INT(11) NULL DEFAULT NULL, + DEVICE_IDENTIFICATION VARCHAR(300) NULL DEFAULT NULL, + OWNER VARCHAR(45) NULL DEFAULT NULL, + TENANT_ID INTEGER DEFAULT 0, + PRIMARY KEY (ID), + CONSTRAINT fk_DM_DEVICE_DM_DEVICE_TYPE2 FOREIGN KEY (DEVICE_TYPE_ID ) + REFERENCES DM_DEVICE_TYPE (ID ) ON DELETE NO ACTION ON UPDATE NO ACTION +); +-- TO:DO - Remove this INSERT sql statement. +Insert into DM_DEVICE_TYPE (NAME) VALUES ('android'); diff --git a/modules/distribution/src/repository/dbscripts/cdm/mysql.sql b/modules/distribution/src/repository/dbscripts/cdm/mysql.sql new file mode 100644 index 00000000..bc04de87 --- /dev/null +++ b/modules/distribution/src/repository/dbscripts/cdm/mysql.sql @@ -0,0 +1,35 @@ +-- ----------------------------------------------------- +-- Table `DM_DEVICE_TYPE` +-- ----------------------------------------------------- +CREATE TABLE IF NOT EXISTS `DM_DEVICE_TYPE` ( + `ID` INT(11) NOT NULL , + `NAME` VARCHAR(300) NULL DEFAULT NULL , + PRIMARY KEY (`ID`) ) +ENGINE = InnoDB +DEFAULT CHARACTER SET = latin1; + + +-- ----------------------------------------------------- +-- Table `DM_DEVICE` +-- ----------------------------------------------------- +CREATE TABLE IF NOT EXISTS `DM_DEVICE` ( + `ID` VARCHAR(20) NOT NULL , + `DESCRIPTION` TEXT NULL DEFAULT NULL , + `NAME` VARCHAR(100) NULL DEFAULT NULL , + `DATE_OF_ENROLLMENT` DATETIME NULL DEFAULT NULL , + `DATE_OF_LAST_UPDATE` DATETIME NULL DEFAULT NULL , + `OWNERSHIP` VARCHAR(45) NULL DEFAULT NULL , + `STATUS` VARCHAR(15) NULL DEFAULT NULL , + `DEVICE_TYPE_ID` INT(11) NULL DEFAULT NULL , + `DEVICE_IDENTIFICATION` VARCHAR(300) NULL DEFAULT NULL , + `OWNER` VARCHAR(45) NULL DEFAULT NULL , + TENANT_ID INTEGER DEFAULT 0, + PRIMARY KEY (`ID`) , + INDEX `fk_DM_DEVICE_DM_DEVICE_TYPE2_idx` (`DEVICE_TYPE_ID` ASC) , + CONSTRAINT `fk_DM_DEVICE_DM_DEVICE_TYPE2` + FOREIGN KEY (`DEVICE_TYPE_ID` ) + REFERENCES `DM_DEVICE_TYPE` (`ID` ) + ON DELETE NO ACTION + ON UPDATE NO ACTION) +ENGINE = InnoDB +DEFAULT CHARACTER SET = latin1; diff --git a/modules/distribution/src/repository/dbscripts/cdm/plugins/h2.sql b/modules/distribution/src/repository/dbscripts/cdm/plugins/h2.sql new file mode 100644 index 00000000..39610edd --- /dev/null +++ b/modules/distribution/src/repository/dbscripts/cdm/plugins/h2.sql @@ -0,0 +1,82 @@ + +-- ----------------------------------------------------- +-- Table `MBL_DEVICE` +-- ----------------------------------------------------- +CREATE TABLE IF NOT EXISTS `MBL_DEVICE` ( + `MOBILE_DEVICE_ID` VARCHAR(45) NOT NULL , + `REG_ID` VARCHAR(45) NULL DEFAULT NULL , + `IMEI` VARCHAR(45) NULL DEFAULT NULL , + `IMSI` VARCHAR(45) NULL DEFAULT NULL , + `OS_VERSION` VARCHAR(45) NULL DEFAULT NULL , + `DEVICE_MODEL` VARCHAR(45) NULL DEFAULT NULL , + `VENDOR` VARCHAR(45) NULL DEFAULT NULL , + `LATITUDE` VARCHAR(45) NULL DEFAULT NULL, + `LONGITUDE` VARCHAR(45) NULL DEFAULT NULL, + PRIMARY KEY (`MOBILE_DEVICE_ID`) ); + + +-- ----------------------------------------------------- +-- Table `MBL_FEATURE` +-- ----------------------------------------------------- +CREATE TABLE IF NOT EXISTS `MBL_FEATURE` ( + `FEATURE_ID` INT NOT NULL AUTO_INCREMENT , + `CODE` VARCHAR(45) NOT NULL , + `NAME` VARCHAR(100) NULL , + `DESCRIPTION` VARCHAR(200) NULL , + PRIMARY KEY (`FEATURE_ID`) ); + +-- ----------------------------------------------------- +-- Table `MBL_OPERATION` +-- ----------------------------------------------------- +CREATE TABLE IF NOT EXISTS `MBL_OPERATION` ( + `OPERATION_ID` INT NOT NULL AUTO_INCREMENT , + `FEATURE_CODE` VARCHAR(45) NOT NULL , + `CREATED_DATE` BIGINT NULL , + PRIMARY KEY (`OPERATION_ID`)); + +-- ----------------------------------------------------- +-- Table `MBL_DEVICE_OPERATION_MAPPING` +-- ----------------------------------------------------- +CREATE TABLE IF NOT EXISTS `MBL_DEVICE_OPERATION_MAPPING` ( + `DEVICE_ID` VARCHAR(45) NOT NULL , + `OPERATION_ID` INT NOT NULL , + `SENT_DATE` BIGINT NULL , + `RECEIVED_DATE` BIGINT NULL , + PRIMARY KEY (`DEVICE_ID`, `OPERATION_ID`) , + CONSTRAINT `fk_MBL_DEVICE_OPERATION_MBL_DEVICE` + FOREIGN KEY (`DEVICE_ID` ) + REFERENCES `MBL_DEVICE` (`MOBILE_DEVICE_ID` ) + ON DELETE NO ACTION + ON UPDATE NO ACTION, + CONSTRAINT `fk_MBL_DEVICE_OPERATION_MBL_OPERATION1` + FOREIGN KEY (`OPERATION_ID` ) + REFERENCES `MBL_OPERATION` (`OPERATION_ID` ) + ON DELETE NO ACTION + ON UPDATE NO ACTION); + +-- ----------------------------------------------------- +-- Table `MBL_OPERATION_PROPERTY` +-- ----------------------------------------------------- +CREATE TABLE IF NOT EXISTS `MBL_OPERATION_PROPERTY` ( + `OPERATION_ID` INT NOT NULL , + `PROPERTY` VARCHAR(45) NOT NULL , + `VALUE` TEXT NULL , + PRIMARY KEY (`OPERATION_ID`, `PROPERTY`) , + CONSTRAINT `fk_MBL_OPERATION_PROPERTY_MBL_OPERATION1` + FOREIGN KEY (`OPERATION_ID` ) + REFERENCES `MBL_OPERATION` (`OPERATION_ID` ) + ON DELETE NO ACTION + ON UPDATE NO ACTION); + +-- ----------------------------------------------------- +-- Table `MBL_FEATURE_PROPERTY` +-- ----------------------------------------------------- +CREATE TABLE IF NOT EXISTS `MBL_FEATURE_PROPERTY` ( + `PROPERTY` VARCHAR(45) NOT NULL , + `FEATURE_ID` VARCHAR(45) NOT NULL , + PRIMARY KEY (`PROPERTY`) , + CONSTRAINT `fk_MBL_FEATURE_PROPERTY_MBL_FEATURE1` + FOREIGN KEY (`FEATURE_ID` ) + REFERENCES `MBL_FEATURE` (`FEATURE_ID` ) + ON DELETE NO ACTION + ON UPDATE NO ACTION); diff --git a/modules/distribution/src/repository/dbscripts/cdm/plugins/mysql.sql b/modules/distribution/src/repository/dbscripts/cdm/plugins/mysql.sql new file mode 100644 index 00000000..10adee8d --- /dev/null +++ b/modules/distribution/src/repository/dbscripts/cdm/plugins/mysql.sql @@ -0,0 +1,99 @@ +-- ----------------------------------------------------- +-- Table `MBL_DEVICE` +-- ----------------------------------------------------- +CREATE TABLE IF NOT EXISTS `MBL_DEVICE` ( + `MOBILE_DEVICE_ID` VARCHAR(45) NOT NULL, + `REG_ID` VARCHAR(45) NULL DEFAULT NULL, + `IMEI` VARCHAR(45) NULL DEFAULT NULL, + `IMSI` VARCHAR(45) NULL DEFAULT NULL, + `OS_VERSION` VARCHAR(45) NULL DEFAULT NULL, + `DEVICE_MODEL` VARCHAR(45) NULL DEFAULT NULL, + `VENDOR` VARCHAR(45) NULL DEFAULT NULL, + `LATITUDE` VARCHAR(45) NULL DEFAULT NULL, + `LONGITUDE` VARCHAR(45) NULL DEFAULT NULL, + PRIMARY KEY (`MOBILE_DEVICE_ID`)) +ENGINE = InnoDB; + + +-- ----------------------------------------------------- +-- Table `MBL_FEATURE` +-- ----------------------------------------------------- +CREATE TABLE IF NOT EXISTS `MBL_FEATURE` ( + `FEATURE_ID` INT NOT NULL AUTO_INCREMENT, + `CODE` VARCHAR(45) NULL, + `NAME` VARCHAR(100) NULL, + `DESCRIPTION` VARCHAR(200) NULL, + PRIMARY KEY (`FEATURE_ID`)) +ENGINE = InnoDB; + + +-- ----------------------------------------------------- +-- Table `MBL_OPERATION` +-- ----------------------------------------------------- +CREATE TABLE IF NOT EXISTS `MBL_OPERATION` ( + `OPERATION_ID` INT NOT NULL AUTO_INCREMENT, + `FEATURE_CODE` VARCHAR(45) NULL, + `CREATED_DATE` INT NULL, + PRIMARY KEY (`OPERATION_ID`)) +ENGINE = InnoDB; + + +-- ----------------------------------------------------- +-- Table `MBL_DEVICE_OPERATION_MAPING` +-- ----------------------------------------------------- +CREATE TABLE IF NOT EXISTS `MBL_DEVICE_OPERATION_MAPPING` ( + `DEVICE_ID` VARCHAR(45) NOT NULL, + `OPERATION_ID` INT NOT NULL, + `SENT_DATE` INT NULL, + `RECEIVED_DATE` INT NULL, + PRIMARY KEY (`DEVICE_ID`, `OPERATION_ID`), + INDEX `fk_MBL_DEVICE_OPERATION_MBL_OPERATION1_idx` (`OPERATION_ID` ASC), + CONSTRAINT `fk_MBL_DEVICE_OPERATION_MBL_DEVICE` + FOREIGN KEY (`DEVICE_ID`) + REFERENCES `MBL_DEVICE` (`MOBILE_DEVICE_ID`) + ON DELETE NO ACTION + ON UPDATE NO ACTION, + CONSTRAINT `fk_MBL_DEVICE_OPERATION_MBL_OPERATION1` + FOREIGN KEY (`OPERATION_ID`) + REFERENCES `MBL_OPERATION` (`OPERATION_ID`) + ON DELETE NO ACTION + ON UPDATE NO ACTION) +ENGINE = InnoDB; + + +-- ----------------------------------------------------- +-- Table `MBL_OPERATION_PROPERTY` +-- ----------------------------------------------------- +CREATE TABLE IF NOT EXISTS `MBL_OPERATION_PROPERTY` ( + `OPERATION_PROPERTY_ID` INT NOT NULL AUTO_INCREMENT, + `OPERATION_ID` INT NULL, + `PROPERTY_ID` INT NULL, + `VALUE` TEXT NULL, + PRIMARY KEY (`OPERATION_PROPERTY_ID`), + INDEX `fk_MBL_OPERATION_PROPERTY_MBL_OPERATION1_idx` (`OPERATION_ID` ASC), + CONSTRAINT `fk_MBL_OPERATION_PROPERTY_MBL_OPERATION1` + FOREIGN KEY (`OPERATION_ID`) + REFERENCES `MBL_OPERATION` (`OPERATION_ID`) + ON DELETE NO ACTION + ON UPDATE NO ACTION) +ENGINE = InnoDB; + + +-- ----------------------------------------------------- +-- Table `MBL_FEATURE_PROPERTY` +-- ----------------------------------------------------- +CREATE TABLE IF NOT EXISTS `MBL_FEATURE_PROPERTY` ( + `PROPERTY_ID` INT NOT NULL AUTO_INCREMENT, + `PROPERTY` VARCHAR(100) NULL, + `FEATURE_ID` VARCHAR(45) NULL, + PRIMARY KEY (`PROPERTY_ID`), + INDEX `fk_MBL_FEATURE_PROPERTY_MBL_FEATURE1_idx` (`FEATURE_ID` ASC), + CONSTRAINT `fk_MBL_FEATURE_PROPERTY_MBL_FEATURE1` + FOREIGN KEY (`FEATURE_ID`) + REFERENCES `MBL_FEATURE` (`FEATURE_ID`) + ON DELETE NO ACTION + ON UPDATE NO ACTION) +ENGINE = InnoDB; + + + diff --git a/modules/distribution/src/repository/resources/build.xml b/modules/distribution/src/repository/resources/build.xml new file mode 100644 index 00000000..d231184c --- /dev/null +++ b/modules/distribution/src/repository/resources/build.xml @@ -0,0 +1,50 @@ + + + + + + ant publish + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/distribution/src/repository/resources/dashboard/dashboard.xml b/modules/distribution/src/repository/resources/dashboard/dashboard.xml new file mode 100644 index 00000000..1c3df925 --- /dev/null +++ b/modules/distribution/src/repository/resources/dashboard/dashboard.xml @@ -0,0 +1,162 @@ + + + + + + + + + + + + + + + + + + + /registry/resource/_system/config/repository/dashboards/gadgets/impact-analysis.xml + + + + + + + + + + + + + + + + + + /carbon/impactAnalysis/impact.xml + + + + + + + + + + + + + + + + + + /registry/resource/_system/config/repository/dashboards/gadgets/life-cycle-info.xml + + + + + + + + + + + + + + + + + + + + 200 + + + + + + + + 200 + 0,3 + + + + + + + Impact Data + + + + + + + {"layout":[{"id":"","width":"50%","layout":[{"id":"10","type":"gadget"}],"type":"columnContainer"},{"id":"","width":"50%","layout":[{"id":"20","type":"gadget"}],"type":"columnContainer"}]} + + + + + + + + + + Lifecycle Data + + + + + + + G1#30 + + + + + + + + + + + + diff --git a/modules/distribution/src/repository/resources/dashboard/gadgets/css/gadgets.css b/modules/distribution/src/repository/resources/dashboard/gadgets/css/gadgets.css new file mode 100644 index 00000000..157e15c5 --- /dev/null +++ b/modules/distribution/src/repository/resources/dashboard/gadgets/css/gadgets.css @@ -0,0 +1,63 @@ +body { + background-color: white; + padding: 0px; + margin: 0px; + font-family: "Lucida Grande","Lucida Sans","Microsoft Sans Serif", "Lucida Sans Unicode","Verdana","Sans-serif","trebuchet ms" !important; + color: #111; + font-size:12px; + font-size-adjust:none; + font-stretch:normal; + font-style:normal; + font-variant:normal; + font-weight:normal; + line-height:1.25em; +} +.main-table { + border: 0px; + padding: 0px; + margin-top: 5px; + margin-bottom: 0px; + margin-left: 0px; + margin-right: 0px; +} +.main-table thead tr th{ + padding: 0px; + text-align: left; + vertical-align: top; + border: 0px; +} +.main-table tbody tr td { + padding: 0px; + text-align: left; + vertical-align: top; + border: 0px; +} +.main-table tr td { + padding: 0px; + text-align: left; + vertical-align: top; + border: 0px; +} +.tablib_selected { + color: #2F7ABD; + font-weight: normal; +} +.pleaseText { + margin-top: 5px; + text-align: center; +} +.titleText { + margin-top: 5px; + text-align: center; +} +select { + font-size: 10px; + margin-bottom: 2px; +} +input { + font-size: 10px; + margin-bottom: 2px; +} +span.userNameText { + font-size: 11px; +} \ No newline at end of file diff --git a/modules/distribution/src/repository/resources/dashboard/gadgets/css/help.css b/modules/distribution/src/repository/resources/dashboard/gadgets/css/help.css new file mode 100644 index 00000000..737de365 --- /dev/null +++ b/modules/distribution/src/repository/resources/dashboard/gadgets/css/help.css @@ -0,0 +1,58 @@ +div#help-wrapper { +background-color:white; +color:#111111; +font-family:"Lucida Grande","Lucida Sans","Microsoft Sans Serif","Lucida Sans Unicode","Verdana","Sans-serif","trebuchet ms"; +font-size:12px; +font-size-adjust:none; +font-stretch:normal; +font-style:normal; +font-variant:normal; +font-weight:normal; +line-height:1.25em; +margin:0; +padding:0; +} +div#help-wrapper p { +margin-left:0; +margin-right:0; +} +td { +} +a:link { +} +a:visited { +} +a:hover { +} +a:active { +} +div#help-wrapper h1 { +color:#F47B20; +font-size:16px; +font-weight:bold; +} + +div#help-wrapper h2 { +color:#111111; +font-size:14px; +font-weight:bold; +} +a img { +border:0 none; +} +table.styled { +border:0 solid #CCCCCC; +border-collapse:collapse; +} +table.styled tr td { +border:1px solid #CCCCCC; +padding:3px; +} +table.styled tr td.subHeader { +border:0 solid #CCCCCC; +font-size:125%; +font-weight:bold; +padding-bottom:5px; +padding-top:10px; +} + diff --git a/modules/distribution/src/repository/resources/dashboard/gadgets/encoder-decoder.js b/modules/distribution/src/repository/resources/dashboard/gadgets/encoder-decoder.js new file mode 100644 index 00000000..412717a7 --- /dev/null +++ b/modules/distribution/src/repository/resources/dashboard/gadgets/encoder-decoder.js @@ -0,0 +1,184 @@ +function urlDecode(str) { + str = str.replace(new RegExp('\\+', 'g'), ' '); + return unescape(str); +} +function urlEncode(str) { + str = escape(str); + str = str.replace(new RegExp('\\+', 'g'), '%2B'); + return str.replace(new RegExp('%20', 'g'), '+'); +} + +var END_OF_INPUT = -1; + +var base64Chars = new Array( + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', + 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', + 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', + 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', + 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', + 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', + 'w', 'x', 'y', 'z', '0', '1', '2', '3', + '4', '5', '6', '7', '8', '9', '+', '/' + ); + +var reverseBase64Chars = new Array(); +for (var i = 0; i < base64Chars.length; i++) { + reverseBase64Chars[base64Chars[i]] = i; +} + +var base64Str; +var base64Count; +function setBase64Str(str) { + base64Str = str; + base64Count = 0; +} +function readBase64() { + if (!base64Str) return END_OF_INPUT; + if (base64Count >= base64Str.length) return END_OF_INPUT; + var c = base64Str.charCodeAt(base64Count) & 0xff; + base64Count++; + return c; +} +function encodeBase64(str) { + setBase64Str(str); + var result = ''; + var inBuffer = new Array(3); + var lineCount = 0; + var done = false; + while (!done && (inBuffer[0] = readBase64()) != END_OF_INPUT) { + inBuffer[1] = readBase64(); + inBuffer[2] = readBase64(); + result += (base64Chars[ inBuffer[0] >> 2 ]); + if (inBuffer[1] != END_OF_INPUT) { + result += (base64Chars [(( inBuffer[0] << 4 ) & 0x30) | (inBuffer[1] >> 4) ]); + if (inBuffer[2] != END_OF_INPUT) { + result += (base64Chars [((inBuffer[1] << 2) & 0x3c) | (inBuffer[2] >> 6) ]); + result += (base64Chars [inBuffer[2] & 0x3F]); + } else { + result += (base64Chars [((inBuffer[1] << 2) & 0x3c)]); + result += ('='); + done = true; + } + } else { + result += (base64Chars [(( inBuffer[0] << 4 ) & 0x30)]); + result += ('='); + result += ('='); + done = true; + } + lineCount += 4; + if (lineCount >= 76) { + result += ('\n'); + lineCount = 0; + } + } + return result; +} +function readReverseBase64() { + if (!base64Str) return END_OF_INPUT; + while (true) { + if (base64Count >= base64Str.length) return END_OF_INPUT; + var nextCharacter = base64Str.charAt(base64Count); + base64Count++; + if (reverseBase64Chars[nextCharacter]) { + return reverseBase64Chars[nextCharacter]; + } + if (nextCharacter == 'A') return 0; + } + return END_OF_INPUT; +} + +function ntos(n) { + n = n.toString(16); + if (n.length == 1) n = "0" + n; + n = "%" + n; + return unescape(n); +} + +function decodeBase64(str) { + setBase64Str(str); + var result = ""; + var inBuffer = new Array(4); + var done = false; + while (!done && (inBuffer[0] = readReverseBase64()) != END_OF_INPUT + && (inBuffer[1] = readReverseBase64()) != END_OF_INPUT) { + inBuffer[2] = readReverseBase64(); + inBuffer[3] = readReverseBase64(); + result += ntos((((inBuffer[0] << 2) & 0xff) | inBuffer[1] >> 4)); + if (inBuffer[2] != END_OF_INPUT) { + result += ntos((((inBuffer[1] << 4) & 0xff) | inBuffer[2] >> 2)); + if (inBuffer[3] != END_OF_INPUT) { + result += ntos((((inBuffer[2] << 6) & 0xff) | inBuffer[3])); + } else { + done = true; + } + } else { + done = true; + } + } + return result; +} + +var digitArray = new Array('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'); +function toHex(n) { + var result = '' + var start = true; + for (var i = 32; i > 0;) { + i -= 4; + var digit = (n >> i) & 0xf; + if (!start || digit != 0) { + start = false; + result += digitArray[digit]; + } + } + return (result == '' ? '0' : result); +} + +function pad(str, len, pad) { + var result = str; + for (var i = str.length; i < len; i++) { + result = pad + result; + } + return result; +} + +function encodeHex(str) { + var result = ""; + for (var i = 0; i < str.length; i++) { + result += pad(toHex(str.charCodeAt(i) & 0xff), 2, '0'); + } + return result; +} + +var hexv = { + "00":0,"01":1,"02":2,"03":3,"04":4,"05":5,"06":6,"07":7,"08":8,"09":9,"0A":10,"0B":11,"0C":12,"0D":13,"0E":14,"0F":15, + "10":16,"11":17,"12":18,"13":19,"14":20,"15":21,"16":22,"17":23,"18":24,"19":25,"1A":26,"1B":27,"1C":28,"1D":29,"1E":30,"1F":31, + "20":32,"21":33,"22":34,"23":35,"24":36,"25":37,"26":38,"27":39,"28":40,"29":41,"2A":42,"2B":43,"2C":44,"2D":45,"2E":46,"2F":47, + "30":48,"31":49,"32":50,"33":51,"34":52,"35":53,"36":54,"37":55,"38":56,"39":57,"3A":58,"3B":59,"3C":60,"3D":61,"3E":62,"3F":63, + "40":64,"41":65,"42":66,"43":67,"44":68,"45":69,"46":70,"47":71,"48":72,"49":73,"4A":74,"4B":75,"4C":76,"4D":77,"4E":78,"4F":79, + "50":80,"51":81,"52":82,"53":83,"54":84,"55":85,"56":86,"57":87,"58":88,"59":89,"5A":90,"5B":91,"5C":92,"5D":93,"5E":94,"5F":95, + "60":96,"61":97,"62":98,"63":99,"64":100,"65":101,"66":102,"67":103,"68":104,"69":105,"6A":106,"6B":107,"6C":108,"6D":109,"6E":110,"6F":111, + "70":112,"71":113,"72":114,"73":115,"74":116,"75":117,"76":118,"77":119,"78":120,"79":121,"7A":122,"7B":123,"7C":124,"7D":125,"7E":126,"7F":127, + "80":128,"81":129,"82":130,"83":131,"84":132,"85":133,"86":134,"87":135,"88":136,"89":137,"8A":138,"8B":139,"8C":140,"8D":141,"8E":142,"8F":143, + "90":144,"91":145,"92":146,"93":147,"94":148,"95":149,"96":150,"97":151,"98":152,"99":153,"9A":154,"9B":155,"9C":156,"9D":157,"9E":158,"9F":159, + "A0":160,"A1":161,"A2":162,"A3":163,"A4":164,"A5":165,"A6":166,"A7":167,"A8":168,"A9":169,"AA":170,"AB":171,"AC":172,"AD":173,"AE":174,"AF":175, + "B0":176,"B1":177,"B2":178,"B3":179,"B4":180,"B5":181,"B6":182,"B7":183,"B8":184,"B9":185,"BA":186,"BB":187,"BC":188,"BD":189,"BE":190,"BF":191, + "C0":192,"C1":193,"C2":194,"C3":195,"C4":196,"C5":197,"C6":198,"C7":199,"C8":200,"C9":201,"CA":202,"CB":203,"CC":204,"CD":205,"CE":206,"CF":207, + "D0":208,"D1":209,"D2":210,"D3":211,"D4":212,"D5":213,"D6":214,"D7":215,"D8":216,"D9":217,"DA":218,"DB":219,"DC":220,"DD":221,"DE":222,"DF":223, + "E0":224,"E1":225,"E2":226,"E3":227,"E4":228,"E5":229,"E6":230,"E7":231,"E8":232,"E9":233,"EA":234,"EB":235,"EC":236,"ED":237,"EE":238,"EF":239, + "F0":240,"F1":241,"F2":242,"F3":243,"F4":244,"F5":245,"F6":246,"F7":247,"F8":248,"F9":249,"FA":250,"FB":251,"FC":252,"FD":253,"FE":254,"FF":255 +}; + +function decodeHex(str) { + str = str.toUpperCase().replace(new RegExp("s/[^0-9A-Z]//g")); + var result = ""; + var nextchar = ""; + for (var i = 0; i < str.length; i++) { + nextchar += str.charAt(i); + if (nextchar.length == 2) { + result += ntos(hexv[nextchar]); + nextchar = ""; + } + } + return result; + +} diff --git a/modules/distribution/src/repository/resources/dashboard/gadgets/flash/digital.swf b/modules/distribution/src/repository/resources/dashboard/gadgets/flash/digital.swf new file mode 100644 index 00000000..edb3af93 Binary files /dev/null and b/modules/distribution/src/repository/resources/dashboard/gadgets/flash/digital.swf differ diff --git a/modules/distribution/src/repository/resources/dashboard/gadgets/flash/funnelgraph.swf b/modules/distribution/src/repository/resources/dashboard/gadgets/flash/funnelgraph.swf new file mode 100644 index 00000000..afd5a6e8 Binary files /dev/null and b/modules/distribution/src/repository/resources/dashboard/gadgets/flash/funnelgraph.swf differ diff --git a/modules/distribution/src/repository/resources/dashboard/gadgets/flash/hclustergraph.swf b/modules/distribution/src/repository/resources/dashboard/gadgets/flash/hclustergraph.swf new file mode 100644 index 00000000..c72f2626 Binary files /dev/null and b/modules/distribution/src/repository/resources/dashboard/gadgets/flash/hclustergraph.swf differ diff --git a/modules/distribution/src/repository/resources/dashboard/gadgets/flash/lineargraph.swf b/modules/distribution/src/repository/resources/dashboard/gadgets/flash/lineargraph.swf new file mode 100644 index 00000000..1cf1156e Binary files /dev/null and b/modules/distribution/src/repository/resources/dashboard/gadgets/flash/lineargraph.swf differ diff --git a/modules/distribution/src/repository/resources/dashboard/gadgets/flash/meter.swf b/modules/distribution/src/repository/resources/dashboard/gadgets/flash/meter.swf new file mode 100644 index 00000000..79fbf80a Binary files /dev/null and b/modules/distribution/src/repository/resources/dashboard/gadgets/flash/meter.swf differ diff --git a/modules/distribution/src/repository/resources/dashboard/gadgets/flash/open-flash-chart.swf b/modules/distribution/src/repository/resources/dashboard/gadgets/flash/open-flash-chart.swf new file mode 100644 index 00000000..bcadfa1b Binary files /dev/null and b/modules/distribution/src/repository/resources/dashboard/gadgets/flash/open-flash-chart.swf differ diff --git a/modules/distribution/src/repository/resources/dashboard/gadgets/flash/wedgegraph.swf b/modules/distribution/src/repository/resources/dashboard/gadgets/flash/wedgegraph.swf new file mode 100644 index 00000000..0420938d Binary files /dev/null and b/modules/distribution/src/repository/resources/dashboard/gadgets/flash/wedgegraph.swf differ diff --git a/modules/distribution/src/repository/resources/dashboard/gadgets/images/bar.jpg b/modules/distribution/src/repository/resources/dashboard/gadgets/images/bar.jpg new file mode 100644 index 00000000..6dfe9e64 Binary files /dev/null and b/modules/distribution/src/repository/resources/dashboard/gadgets/images/bar.jpg differ diff --git a/modules/distribution/src/repository/resources/dashboard/gadgets/images/digital.jpg b/modules/distribution/src/repository/resources/dashboard/gadgets/images/digital.jpg new file mode 100644 index 00000000..6e290498 Binary files /dev/null and b/modules/distribution/src/repository/resources/dashboard/gadgets/images/digital.jpg differ diff --git a/modules/distribution/src/repository/resources/dashboard/gadgets/images/down-icon.gif b/modules/distribution/src/repository/resources/dashboard/gadgets/images/down-icon.gif new file mode 100644 index 00000000..297dcd18 Binary files /dev/null and b/modules/distribution/src/repository/resources/dashboard/gadgets/images/down-icon.gif differ diff --git a/modules/distribution/src/repository/resources/dashboard/gadgets/images/gauge.jpg b/modules/distribution/src/repository/resources/dashboard/gadgets/images/gauge.jpg new file mode 100644 index 00000000..708f689e Binary files /dev/null and b/modules/distribution/src/repository/resources/dashboard/gadgets/images/gauge.jpg differ diff --git a/modules/distribution/src/repository/resources/dashboard/gadgets/images/iogauge.jpg b/modules/distribution/src/repository/resources/dashboard/gadgets/images/iogauge.jpg new file mode 100644 index 00000000..be2809f6 Binary files /dev/null and b/modules/distribution/src/repository/resources/dashboard/gadgets/images/iogauge.jpg differ diff --git a/modules/distribution/src/repository/resources/dashboard/gadgets/images/meter.jpg b/modules/distribution/src/repository/resources/dashboard/gadgets/images/meter.jpg new file mode 100644 index 00000000..0f5520db Binary files /dev/null and b/modules/distribution/src/repository/resources/dashboard/gadgets/images/meter.jpg differ diff --git a/modules/distribution/src/repository/resources/dashboard/gadgets/images/rmeter.jpg b/modules/distribution/src/repository/resources/dashboard/gadgets/images/rmeter.jpg new file mode 100644 index 00000000..3d8a29ce Binary files /dev/null and b/modules/distribution/src/repository/resources/dashboard/gadgets/images/rmeter.jpg differ diff --git a/modules/distribution/src/repository/resources/dashboard/gadgets/images/temp.jpg b/modules/distribution/src/repository/resources/dashboard/gadgets/images/temp.jpg new file mode 100644 index 00000000..c65157fa Binary files /dev/null and b/modules/distribution/src/repository/resources/dashboard/gadgets/images/temp.jpg differ diff --git a/modules/distribution/src/repository/resources/dashboard/gadgets/impact-analysis.xml b/modules/distribution/src/repository/resources/dashboard/gadgets/impact-analysis.xml new file mode 100644 index 00000000..39c519d8 --- /dev/null +++ b/modules/distribution/src/repository/resources/dashboard/gadgets/impact-analysis.xml @@ -0,0 +1,223 @@ + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + +]]> + +
+ + + + + + + +
Available Services :
+ +
+
+ + + +]]> + +
+ +
+ diff --git a/modules/distribution/src/repository/resources/dashboard/gadgets/js/count-graph.js b/modules/distribution/src/repository/resources/dashboard/gadgets/js/count-graph.js new file mode 100644 index 00000000..4d30f3bb --- /dev/null +++ b/modules/distribution/src/repository/resources/dashboard/gadgets/js/count-graph.js @@ -0,0 +1,23 @@ + +var countGraph; + +function isNumeric(sText){ + var validChars = "0123456789."; + var isNumber = true; + var character; + for (var i = 0; i < sText.length && isNumber == true; i++) { + character = sText.charAt(i); + if (validChars.indexOf(character) == -1) { + isNumber = false; + } + } + return isNumber; +} + +function initCountGraph(memoryXScale) { + if (memoryXScale < 1 || !isNumeric(memoryXScale)) { + return; + } + countGraph = new carbonGraph(memoryXScale); +} + diff --git a/modules/distribution/src/repository/resources/dashboard/gadgets/js/endpoint-count.js b/modules/distribution/src/repository/resources/dashboard/gadgets/js/endpoint-count.js new file mode 100644 index 00000000..dd7e3d64 --- /dev/null +++ b/modules/distribution/src/repository/resources/dashboard/gadgets/js/endpoint-count.js @@ -0,0 +1,31 @@ +// Memory +var endpointInvocationGraph; + +function initStats(memoryXScale) { + if (memoryXScale != null) { + initReqCountGraphs(memoryXScale); + } else { + initReqCountGraphs(30); + } +} + +function isNumeric(sText){ + var validChars = "0123456789."; + var isNumber = true; + var character; + for (var i = 0; i < sText.length && isNumber == true; i++) { + character = sText.charAt(i); + if (validChars.indexOf(character) == -1) { + isNumber = false; + } + } + return isNumber; +} + +function initReqCountGraphs(memoryXScale) { + if (memoryXScale < 1 || !isNumeric(memoryXScale)) { + return; + } + endpointInvocationGraph = new carbonGraph(memoryXScale); +} + diff --git a/modules/distribution/src/repository/resources/dashboard/gadgets/js/excanvas.js b/modules/distribution/src/repository/resources/dashboard/gadgets/js/excanvas.js new file mode 100644 index 00000000..7914cb93 --- /dev/null +++ b/modules/distribution/src/repository/resources/dashboard/gadgets/js/excanvas.js @@ -0,0 +1,785 @@ +// Copyright 2006 Google Inc. +// +// 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. + + +// Known Issues: +// +// * Patterns are not implemented. +// * Radial gradient are not implemented. The VML version of these look very +// different from the canvas one. +// * Clipping paths are not implemented. +// * Coordsize. The width and height attribute have higher priority than the +// width and height style values which isn't correct. +// * Painting mode isn't implemented. +// * Canvas width/height should is using content-box by default. IE in +// Quirks mode will draw the canvas using border-box. Either change your +// doctype to HTML5 +// (http://www.whatwg.org/specs/web-apps/current-work/#the-doctype) +// or use Box Sizing Behavior from WebFX +// (http://webfx.eae.net/dhtml/boxsizing/boxsizing.html) +// * Optimize. There is always room for speed improvements. + +// only add this code if we do not already have a canvas implementation +if (!window.CanvasRenderingContext2D) { + +(function () { + + // alias some functions to make (compiled) code shorter + var m = Math; + var mr = m.round; + var ms = m.sin; + var mc = m.cos; + + // this is used for sub pixel precision + var Z = 10; + var Z2 = Z / 2; + + var G_vmlCanvasManager_ = { + init: function (opt_doc) { + var doc = opt_doc || document; + if (/MSIE/.test(navigator.userAgent) && !window.opera) { + var self = this; + doc.attachEvent("onreadystatechange", function () { + self.init_(doc); + }); + } + }, + + init_: function (doc) { + if (doc.readyState == "complete") { + // create xmlns + if (!doc.namespaces["g_vml_"]) { + doc.namespaces.add("g_vml_", "urn:schemas-microsoft-com:vml"); + } + + // setup default css + var ss = doc.createStyleSheet(); + ss.cssText = "canvas{display:inline-block;overflow:hidden;" + + // default size is 300x150 in Gecko and Opera + "text-align:left;width:300px;height:150px}" + + "g_vml_\\:*{behavior:url(#default#VML)}"; + + // find all canvas elements + var els = doc.getElementsByTagName("canvas"); + for (var i = 0; i < els.length; i++) { + if (!els[i].getContext) { + this.initElement(els[i]); + } + } + } + }, + + fixElement_: function (el) { + // in IE before version 5.5 we would need to add HTML: to the tag name + // but we do not care about IE before version 6 + var outerHTML = el.outerHTML; + + var newEl = el.ownerDocument.createElement(outerHTML); + // if the tag is still open IE has created the children as siblings and + // it has also created a tag with the name "/FOO" + if (outerHTML.slice(-2) != "/>") { + var tagName = "/" + el.tagName; + var ns; + // remove content + while ((ns = el.nextSibling) && ns.tagName != tagName) { + ns.removeNode(); + } + // remove the incorrect closing tag + if (ns) { + ns.removeNode(); + } + } + el.parentNode.replaceChild(newEl, el); + return newEl; + }, + + /** + * Public initializes a canvas element so that it can be used as canvas + * element from now on. This is called automatically before the page is + * loaded but if you are creating elements using createElement you need to + * make sure this is called on the element. + * @param {HTMLElement} el The canvas element to initialize. + * @return {HTMLElement} the element that was created. + */ + initElement: function (el) { + el = this.fixElement_(el); + el.getContext = function () { + if (this.context_) { + return this.context_; + } + return this.context_ = new CanvasRenderingContext2D_(this); + }; + + // do not use inline function because that will leak memory + el.attachEvent('onpropertychange', onPropertyChange); + el.attachEvent('onresize', onResize); + + var attrs = el.attributes; + if (attrs.width && attrs.width.specified) { + // TODO: use runtimeStyle and coordsize + // el.getContext().setWidth_(attrs.width.nodeValue); + el.style.width = attrs.width.nodeValue + "px"; + } else { + el.width = el.clientWidth; + } + if (attrs.height && attrs.height.specified) { + // TODO: use runtimeStyle and coordsize + // el.getContext().setHeight_(attrs.height.nodeValue); + el.style.height = attrs.height.nodeValue + "px"; + } else { + el.height = el.clientHeight; + } + //el.getContext().setCoordsize_() + return el; + } + }; + + function onPropertyChange(e) { + var el = e.srcElement; + + switch (e.propertyName) { + case 'width': + el.style.width = el.attributes.width.nodeValue + "px"; + el.getContext().clearRect(); + break; + case 'height': + el.style.height = el.attributes.height.nodeValue + "px"; + el.getContext().clearRect(); + break; + } + } + + function onResize(e) { + var el = e.srcElement; + if (el.firstChild) { + el.firstChild.style.width = el.clientWidth + 'px'; + el.firstChild.style.height = el.clientHeight + 'px'; + } + } + + G_vmlCanvasManager_.init(); + + // precompute "00" to "FF" + var dec2hex = []; + for (var i = 0; i < 16; i++) { + for (var j = 0; j < 16; j++) { + dec2hex[i * 16 + j] = i.toString(16) + j.toString(16); + } + } + + function createMatrixIdentity() { + return [ + [1, 0, 0], + [0, 1, 0], + [0, 0, 1] + ]; + } + + function matrixMultiply(m1, m2) { + var result = createMatrixIdentity(); + + for (var x = 0; x < 3; x++) { + for (var y = 0; y < 3; y++) { + var sum = 0; + + for (var z = 0; z < 3; z++) { + sum += m1[x][z] * m2[z][y]; + } + + result[x][y] = sum; + } + } + return result; + } + + function copyState(o1, o2) { + o2.fillStyle = o1.fillStyle; + o2.lineCap = o1.lineCap; + o2.lineJoin = o1.lineJoin; + o2.lineWidth = o1.lineWidth; + o2.miterLimit = o1.miterLimit; + o2.shadowBlur = o1.shadowBlur; + o2.shadowColor = o1.shadowColor; + o2.shadowOffsetX = o1.shadowOffsetX; + o2.shadowOffsetY = o1.shadowOffsetY; + o2.strokeStyle = o1.strokeStyle; + o2.arcScaleX_ = o1.arcScaleX_; + o2.arcScaleY_ = o1.arcScaleY_; + } + + function processStyle(styleString) { + var str, alpha = 1; + + styleString = String(styleString); + if (styleString.substring(0, 3) == "rgb") { + var start = styleString.indexOf("(", 3); + var end = styleString.indexOf(")", start + 1); + var guts = styleString.substring(start + 1, end).split(","); + + str = "#"; + for (var i = 0; i < 3; i++) { + str += dec2hex[Number(guts[i])]; + } + + if ((guts.length == 4) && (styleString.substr(3, 1) == "a")) { + alpha = guts[3]; + } + } else { + str = styleString; + } + + return [str, alpha]; + } + + function processLineCap(lineCap) { + switch (lineCap) { + case "butt": + return "flat"; + case "round": + return "round"; + case "square": + default: + return "square"; + } + } + + /** + * This class implements CanvasRenderingContext2D interface as described by + * the WHATWG. + * @param {HTMLElement} surfaceElement The element that the 2D context should + * be associated with + */ + function CanvasRenderingContext2D_(surfaceElement) { + this.m_ = createMatrixIdentity(); + + this.mStack_ = []; + this.aStack_ = []; + this.currentPath_ = []; + + // Canvas context properties + this.strokeStyle = "#000"; + this.fillStyle = "#000"; + + this.lineWidth = 1; + this.lineJoin = "miter"; + this.lineCap = "butt"; + this.miterLimit = Z * 1; + this.globalAlpha = 1; + this.canvas = surfaceElement; + + var el = surfaceElement.ownerDocument.createElement('div'); + el.style.width = surfaceElement.clientWidth + 'px'; + el.style.height = surfaceElement.clientHeight + 'px'; + el.style.overflow = 'hidden'; + el.style.position = 'absolute'; + surfaceElement.appendChild(el); + + this.element_ = el; + this.arcScaleX_ = 1; + this.arcScaleY_ = 1; + } + + var contextPrototype = CanvasRenderingContext2D_.prototype; + contextPrototype.clearRect = function() { + this.element_.innerHTML = ""; + this.currentPath_ = []; + }; + + contextPrototype.beginPath = function() { + // TODO: Branch current matrix so that save/restore has no effect + // as per safari docs. + + this.currentPath_ = []; + }; + + contextPrototype.moveTo = function(aX, aY) { + this.currentPath_.push({type: "moveTo", x: aX, y: aY}); + this.currentX_ = aX; + this.currentY_ = aY; + }; + + contextPrototype.lineTo = function(aX, aY) { + this.currentPath_.push({type: "lineTo", x: aX, y: aY}); + this.currentX_ = aX; + this.currentY_ = aY; + }; + + contextPrototype.bezierCurveTo = function(aCP1x, aCP1y, + aCP2x, aCP2y, + aX, aY) { + this.currentPath_.push({type: "bezierCurveTo", + cp1x: aCP1x, + cp1y: aCP1y, + cp2x: aCP2x, + cp2y: aCP2y, + x: aX, + y: aY}); + this.currentX_ = aX; + this.currentY_ = aY; + }; + + contextPrototype.quadraticCurveTo = function(aCPx, aCPy, aX, aY) { + // the following is lifted almost directly from + // http://developer.mozilla.org/en/docs/Canvas_tutorial:Drawing_shapes + var cp1x = this.currentX_ + 2.0 / 3.0 * (aCPx - this.currentX_); + var cp1y = this.currentY_ + 2.0 / 3.0 * (aCPy - this.currentY_); + var cp2x = cp1x + (aX - this.currentX_) / 3.0; + var cp2y = cp1y + (aY - this.currentY_) / 3.0; + this.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, aX, aY); + }; + + contextPrototype.arc = function(aX, aY, aRadius, + aStartAngle, aEndAngle, aClockwise) { + aRadius *= Z; + var arcType = aClockwise ? "at" : "wa"; + + var xStart = aX + (mc(aStartAngle) * aRadius) - Z2; + var yStart = aY + (ms(aStartAngle) * aRadius) - Z2; + + var xEnd = aX + (mc(aEndAngle) * aRadius) - Z2; + var yEnd = aY + (ms(aEndAngle) * aRadius) - Z2; + + // IE won't render arches drawn counter clockwise if xStart == xEnd. + if (xStart == xEnd && !aClockwise) { + xStart += 0.125; // Offset xStart by 1/80 of a pixel. Use something + // that can be represented in binary + } + + this.currentPath_.push({type: arcType, + x: aX, + y: aY, + radius: aRadius, + xStart: xStart, + yStart: yStart, + xEnd: xEnd, + yEnd: yEnd}); + + }; + + contextPrototype.rect = function(aX, aY, aWidth, aHeight) { + this.moveTo(aX, aY); + this.lineTo(aX + aWidth, aY); + this.lineTo(aX + aWidth, aY + aHeight); + this.lineTo(aX, aY + aHeight); + this.closePath(); + }; + + contextPrototype.strokeRect = function(aX, aY, aWidth, aHeight) { + // Will destroy any existing path (same as FF behaviour) + this.beginPath(); + this.moveTo(aX, aY); + this.lineTo(aX + aWidth, aY); + this.lineTo(aX + aWidth, aY + aHeight); + this.lineTo(aX, aY + aHeight); + this.closePath(); + this.stroke(); + }; + + contextPrototype.fillRect = function(aX, aY, aWidth, aHeight) { + // Will destroy any existing path (same as FF behaviour) + this.beginPath(); + this.moveTo(aX, aY); + this.lineTo(aX + aWidth, aY); + this.lineTo(aX + aWidth, aY + aHeight); + this.lineTo(aX, aY + aHeight); + this.closePath(); + this.fill(); + }; + + contextPrototype.createLinearGradient = function(aX0, aY0, aX1, aY1) { + var gradient = new CanvasGradient_("gradient"); + return gradient; + }; + + contextPrototype.createRadialGradient = function(aX0, aY0, + aR0, aX1, + aY1, aR1) { + var gradient = new CanvasGradient_("gradientradial"); + gradient.radius1_ = aR0; + gradient.radius2_ = aR1; + gradient.focus_.x = aX0; + gradient.focus_.y = aY0; + return gradient; + }; + + contextPrototype.drawImage = function (image, var_args) { + var dx, dy, dw, dh, sx, sy, sw, sh; + + // to find the original width we overide the width and height + var oldRuntimeWidth = image.runtimeStyle.width; + var oldRuntimeHeight = image.runtimeStyle.height; + image.runtimeStyle.width = 'auto'; + image.runtimeStyle.height = 'auto'; + + // get the original size + var w = image.width; + var h = image.height; + + // and remove overides + image.runtimeStyle.width = oldRuntimeWidth; + image.runtimeStyle.height = oldRuntimeHeight; + + if (arguments.length == 3) { + dx = arguments[1]; + dy = arguments[2]; + sx = sy = 0; + sw = dw = w; + sh = dh = h; + } else if (arguments.length == 5) { + dx = arguments[1]; + dy = arguments[2]; + dw = arguments[3]; + dh = arguments[4]; + sx = sy = 0; + sw = w; + sh = h; + } else if (arguments.length == 9) { + sx = arguments[1]; + sy = arguments[2]; + sw = arguments[3]; + sh = arguments[4]; + dx = arguments[5]; + dy = arguments[6]; + dw = arguments[7]; + dh = arguments[8]; + } else { + throw "Invalid number of arguments"; + } + + var d = this.getCoords_(dx, dy); + + var w2 = sw / 2; + var h2 = sh / 2; + + var vmlStr = []; + + var W = 10; + var H = 10; + + // For some reason that I've now forgotten, using divs didn't work + vmlStr.push(' ' , + '', + ''); + + this.element_.insertAdjacentHTML("BeforeEnd", + vmlStr.join("")); + }; + + contextPrototype.stroke = function(aFill) { + var lineStr = []; + var lineOpen = false; + var a = processStyle(aFill ? this.fillStyle : this.strokeStyle); + var color = a[0]; + var opacity = a[1] * this.globalAlpha; + + var W = 10; + var H = 10; + + lineStr.push(' max.x) { + max.x = c.x; + } + if (min.y == null || c.y < min.y) { + min.y = c.y; + } + if (max.y == null || c.y > max.y) { + max.y = c.y; + } + } + } + lineStr.push(' ">'); + + if (typeof this.fillStyle == "object") { + var focus = {x: "50%", y: "50%"}; + var width = (max.x - min.x); + var height = (max.y - min.y); + var dimension = (width > height) ? width : height; + + focus.x = mr((this.fillStyle.focus_.x / width) * 100 + 50) + "%"; + focus.y = mr((this.fillStyle.focus_.y / height) * 100 + 50) + "%"; + + var colors = []; + + // inside radius (%) + if (this.fillStyle.type_ == "gradientradial") { + var inside = (this.fillStyle.radius1_ / dimension * 100); + + // percentage that outside radius exceeds inside radius + var expansion = (this.fillStyle.radius2_ / dimension * 100) - inside; + } else { + var inside = 0; + var expansion = 100; + } + + var insidecolor = {offset: null, color: null}; + var outsidecolor = {offset: null, color: null}; + + // We need to sort 'colors' by percentage, from 0 > 100 otherwise ie + // won't interpret it correctly + this.fillStyle.colors_.sort(function (cs1, cs2) { + return cs1.offset - cs2.offset; + }); + + for (var i = 0; i < this.fillStyle.colors_.length; i++) { + var fs = this.fillStyle.colors_[i]; + + colors.push( (fs.offset * expansion) + inside, "% ", fs.color, ","); + + if (fs.offset > insidecolor.offset || insidecolor.offset == null) { + insidecolor.offset = fs.offset; + insidecolor.color = fs.color; + } + + if (fs.offset < outsidecolor.offset || outsidecolor.offset == null) { + outsidecolor.offset = fs.offset; + outsidecolor.color = fs.color; + } + } + colors.pop(); + + lineStr.push(''); + } else if (aFill) { + lineStr.push(''); + } else { + lineStr.push( + '' + ); + } + + lineStr.push(""); + + this.element_.insertAdjacentHTML("beforeEnd", lineStr.join("")); + + //this.currentPath_ = []; + }; + + contextPrototype.fill = function() { + this.stroke(true); + }; + + contextPrototype.closePath = function() { + this.currentPath_.push({type: "close"}); + }; + + /** + * @private + */ + contextPrototype.getCoords_ = function(aX, aY) { + return { + x: Z * (aX * this.m_[0][0] + aY * this.m_[1][0] + this.m_[2][0]) - Z2, + y: Z * (aX * this.m_[0][1] + aY * this.m_[1][1] + this.m_[2][1]) - Z2 + } + }; + + contextPrototype.save = function() { + var o = {}; + copyState(this, o); + this.aStack_.push(o); + this.mStack_.push(this.m_); + this.m_ = matrixMultiply(createMatrixIdentity(), this.m_); + }; + + contextPrototype.restore = function() { + copyState(this.aStack_.pop(), this); + this.m_ = this.mStack_.pop(); + }; + + contextPrototype.translate = function(aX, aY) { + var m1 = [ + [1, 0, 0], + [0, 1, 0], + [aX, aY, 1] + ]; + + this.m_ = matrixMultiply(m1, this.m_); + }; + + contextPrototype.rotate = function(aRot) { + var c = mc(aRot); + var s = ms(aRot); + + var m1 = [ + [c, s, 0], + [-s, c, 0], + [0, 0, 1] + ]; + + this.m_ = matrixMultiply(m1, this.m_); + }; + + contextPrototype.scale = function(aX, aY) { + this.arcScaleX_ *= aX; + this.arcScaleY_ *= aY; + var m1 = [ + [aX, 0, 0], + [0, aY, 0], + [0, 0, 1] + ]; + + this.m_ = matrixMultiply(m1, this.m_); + }; + + /******** STUBS ********/ + contextPrototype.clip = function() { + // TODO: Implement + }; + + contextPrototype.arcTo = function() { + // TODO: Implement + }; + + contextPrototype.createPattern = function() { + return new CanvasPattern_; + }; + + // Gradient / Pattern Stubs + function CanvasGradient_(aType) { + this.type_ = aType; + this.radius1_ = 0; + this.radius2_ = 0; + this.colors_ = []; + this.focus_ = {x: 0, y: 0}; + } + + CanvasGradient_.prototype.addColorStop = function(aOffset, aColor) { + aColor = processStyle(aColor); + this.colors_.push({offset: 1-aOffset, color: aColor}); + }; + + function CanvasPattern_() {} + + // set up externs + G_vmlCanvasManager = G_vmlCanvasManager_; + CanvasRenderingContext2D = CanvasRenderingContext2D_; + CanvasGradient = CanvasGradient_; + CanvasPattern = CanvasPattern_; + +})(); + +} // if diff --git a/modules/distribution/src/repository/resources/dashboard/gadgets/js/graph.js b/modules/distribution/src/repository/resources/dashboard/gadgets/js/graph.js new file mode 100644 index 00000000..61824afb --- /dev/null +++ b/modules/distribution/src/repository/resources/dashboard/gadgets/js/graph.js @@ -0,0 +1,44 @@ +/*This js mainly concern with the data structures related to graphs*/ +function carbonGraph(xscale) { + this.array = new Array(); + for (var i = 0; i < xscale; i++) { + this.array[i] = [i, 0.0]; + } + this.xscale = xscale; +} + +function getData() { + return this.array; +} + +function addData(newValue) { + + //shift to left + for (var i = 0; i < this.xscale - 1; i++) { + this.array[i] = [i,this.array[i + 1][1]]; // (x,y) + } + + //add the value to the last postion + this.array[this.xscale - 1] = [this.xscale - 1,newValue]; +} + +function graphTickGenerator() { + var tickArray = []; + var startTick = 10; + var i = startTick - 1; + var weight = this.xscale / 10; + do { + var t = (startTick - i) * weight - 1; + var v = i * weight; + if (v == 0) { + v = "0"; + } + tickArray.push([t, v]); + i--; + } while (i > -1); + return tickArray; +} + +carbonGraph.prototype.get = getData; +carbonGraph.prototype.add = addData; +carbonGraph.prototype.tick = graphTickGenerator; \ No newline at end of file diff --git a/modules/distribution/src/repository/resources/dashboard/gadgets/js/help-populator.js b/modules/distribution/src/repository/resources/dashboard/gadgets/js/help-populator.js new file mode 100644 index 00000000..688d4606 --- /dev/null +++ b/modules/distribution/src/repository/resources/dashboard/gadgets/js/help-populator.js @@ -0,0 +1,43 @@ +function clientSideInclude(id, url) { + var req = false; + // For Safari, Firefox, and other non-MS browsers + if (window.XMLHttpRequest) { + try { + req = new XMLHttpRequest(); + } catch (e) { + req = false; + } + } else if (window.ActiveXObject) { + // For Internet Explorer on Windows + try { + req = new ActiveXObject("Msxml2.XMLHTTP"); + } catch (e) { + try { + req = new ActiveXObject("Microsoft.XMLHTTP"); + } catch (e) { + req = false; + } + } + } + var element = document.getElementById(id); + if (!element) { + alert("Bad id " + id + + "passed to clientSideInclude." + + "You need a div or span element " + + "with this id in your page."); + return; + } + if (req) { + // Synchronous request, wait till we have it all + req.open('GET', url, false); + req.send(null); + element.innerHTML = req.responseText; + } else { + element.innerHTML = + "Sorry, your browser does not support " + + "XMLHTTPRequest objects. This page requires " + + "Internet Explorer 5 or better for Windows, " + + "or Firefox for any system, or Safari. Other " + + "compatible browsers may also exist."; + } +} diff --git a/modules/distribution/src/repository/resources/dashboard/gadgets/js/jit.js b/modules/distribution/src/repository/resources/dashboard/gadgets/js/jit.js new file mode 100644 index 00000000..269fbc7d --- /dev/null +++ b/modules/distribution/src/repository/resources/dashboard/gadgets/js/jit.js @@ -0,0 +1,9049 @@ +(function () { + +/* + File: Core.js + + Description: + + Provides common utility functions and the Class object used internally by the library. + + Also provides the object for manipulating JSON tree structures + + Some of the Basic utility functions and the Class system are based in the MooTools Framework . Copyright (c) 2006-2009 Valerio Proietti, . MIT license . + + Author: + + Nicolas Garcia Belmonte + + Copyright: + + Copyright 2008-2009 by Nicolas Garcia Belmonte. + + Homepage: + + + + Version: + + 1.1.3 + + License: + + BSD License + +> Redistribution and use in source and binary forms, with or without +> modification, are permitted provided that the following conditions are met: +> * Redistributions of source code must retain the above copyright +> notice, this list of conditions and the following disclaimer. +> * Redistributions in binary form must reproduce the above copyright +> notice, this list of conditions and the following disclaimer in the +> documentation and/or other materials provided with the distribution. +> * Neither the name of the organization nor the +> names of its contributors may be used to endorse or promote products +> derived from this software without specific prior written permission. +> +> THIS SOFTWARE IS PROVIDED BY Nicolas Garcia Belmonte ``AS IS'' AND ANY +> EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +> WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +> DISCLAIMED. IN NO EVENT SHALL Nicolas Garcia Belmonte BE LIABLE FOR ANY +> DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +> (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +> LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +> ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +> (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +> SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + + +function $empty() {}; + +function $extend(original, extended){ + for (var key in (extended || {})) original[key] = extended[key]; + return original; +}; + +function $lambda(value){ + return (typeof value == 'function') ? value : function(){ + return value; + }; +}; + +var $time = Date.now || function(){ + return +new Date; +}; + +function $splat(obj){ + var type = $type(obj); + return (type) ? ((type != 'array') ? [obj] : obj) : []; +}; + +var $type = function(elem) { + return $type.s.call(elem).match(/^\[object\s(.*)\]$/)[1].toLowerCase(); +}; +$type.s = Object.prototype.toString; + +function $each(iterable, fn){ + var type = $type(iterable); + if(type == 'object') { + for (var key in iterable) fn(iterable[key], key); + } else { + for(var i=0; i < iterable.length; i++) fn(iterable[i], i); + } +}; + +function $merge(){ + var mix = {}; + for (var i = 0, l = arguments.length; i < l; i++){ + var object = arguments[i]; + if ($type(object) != 'object') continue; + for (var key in object){ + var op = object[key], mp = mix[key]; + mix[key] = (mp && $type(op) == 'object' && $type(mp) == 'object') ? $merge(mp, op) : $unlink(op); + } + } + return mix; +}; + +function $unlink(object){ + var unlinked; + switch ($type(object)){ + case 'object': + unlinked = {}; + for (var p in object) unlinked[p] = $unlink(object[p]); + break; + case 'array': + unlinked = []; + for (var i = 0, l = object.length; i < l; i++) unlinked[i] = $unlink(object[i]); + break; + default: return object; + } + return unlinked; +}; + +function $rgbToHex(srcArray, array){ + if (srcArray.length < 3) return null; + if (srcArray.length == 4 && srcArray[3] == 0 && !array) return 'transparent'; + var hex = []; + for (var i = 0; i < 3; i++){ + var bit = (srcArray[i] - 0).toString(16); + hex.push((bit.length == 1) ? '0' + bit : bit); + } + return (array) ? hex : '#' + hex.join(''); +}; + +function $destroy(elem) { + $clean(elem); + if(elem.parentNode) elem.parentNode.removeChild(elem); + if(elem.clearAttributes) elem.clearAttributes(); +}; + +function $clean(elem) { + for(var ch = elem.childNodes, i=0; i < ch.length; i++) { + $destroy(ch[i]); + } +}; + +function $addEvent(obj, type, fn) { + if (obj.addEventListener) + obj.addEventListener(type, fn, false); + else + obj.attachEvent('on' + type, fn); +}; + +function $hasClass(obj, klass) { + return (' ' + obj.className + ' ').indexOf(' ' + klass + ' ') > -1; +}; + +function $addClass(obj, klass) { + if(!$hasClass(obj, klass)) obj.className = (obj.className + " " + klass); +}; + +function $removeClass(obj, klass) { + obj.className = obj.className.replace(new RegExp('(^|\\s)' + klass + '(?:\\s|$)'), '$1'); +}; + +function $get(id) { + return document.getElementById(id); +}; + +var Class = function(properties){ + properties = properties || {}; + var klass = function(){ +// not defining any attributes in Class properties. +// for (var key in this){ +// if (typeof this[key] != 'function') this[key] = $unlink(this[key]); +// } + this.constructor = klass; + if (Class.prototyping) return this; + var instance = (this.initialize) ? this.initialize.apply(this, arguments) : this; + return instance; + }; + + for (var mutator in Class.Mutators){ + if (!properties[mutator]) continue; + properties = Class.Mutators[mutator](properties, properties[mutator]); + delete properties[mutator]; + } + + $extend(klass, this); + klass.constructor = Class; + klass.prototype = properties; + return klass; +}; + +Class.Mutators = { + + Extends: function(self, klass){ + Class.prototyping = klass.prototype; + var subclass = new klass; + delete subclass.parent; + subclass = Class.inherit(subclass, self); + delete Class.prototyping; + return subclass; + }, + + Implements: function(self, klasses){ + $each($splat(klasses), function(klass){ + Class.prototying = klass; + $extend(self, ($type(klass) == 'function') ? new klass : klass); + delete Class.prototyping; + }); + return self; + } + +}; + +$extend(Class, { + + inherit: function(object, properties){ + var caller = arguments.callee.caller; + for (var key in properties){ + var override = properties[key]; + var previous = object[key]; + var type = $type(override); + if (previous && type == 'function'){ + if (override != previous){ + if (caller){ + override.__parent = previous; + object[key] = override; + } else { + Class.override(object, key, override); + } + } + } else if(type == 'object'){ + object[key] = $merge(previous, override); + } else { + object[key] = override; + } + } + + if (caller) object.parent = function(){ + return arguments.callee.caller.__parent.apply(this, arguments); + }; + + return object; + }, + + override: function(object, name, method){ + var parent = Class.prototyping; + if (parent && object[name] != parent[name]) parent = null; + var override = function(){ + var previous = this.parent; + this.parent = parent ? parent[name] : object[name]; + var value = method.apply(this, arguments); + this.parent = previous; + return value; + }; + object[name] = override; + } + +}); + + +Class.prototype.implement = function(){ + var proto = this.prototype; + $each(Array.prototype.slice.call(arguments || []), function(properties){ + Class.inherit(proto, properties); + }); + return this; +}; + +/* + Object: TreeUtil + + Some common JSON tree manipulation methods. +*/ +this.TreeUtil = { + + /* + Method: prune + + Clears all tree nodes having depth greater than maxLevel. + + Parameters: + + tree - A JSON tree object. For more information please see . + maxLevel - An integer specifying the maximum level allowed for this tree. All nodes having depth greater than max level will be deleted. + + */ + prune: function(tree, maxLevel) { + this.each(tree, function(elem, i) { + if(i == maxLevel && elem.children) { + delete elem.children; + elem.children = []; + } + }); + }, + + /* + Method: getParent + + Returns the parent node of the node having _id_ as id. + + Parameters: + + tree - A JSON tree object. See also . + id - The _id_ of the child node whose parent will be returned. + + Returns: + + A tree JSON node if any, or false otherwise. + + */ + getParent: function(tree, id) { + if(tree.id == id) return false; + var ch = tree.children; + if(ch && ch.length > 0) { + for(var i=0; i. + id - A node *unique* identifier. + + Returns: + + A subtree having a root node matching the given id. Returns null if no subtree matching the id is found. + + */ + getSubtree: function(tree, id) { + if(tree.id == id) return tree; + for(var i=0, ch=tree.children; i. + maxLevel - _optional_ A subtree's max level. + + Returns: + + An array having objects with two properties. + + - The _node_ property contains the leaf node. + - The _level_ property specifies the depth of the node. + + */ + getLeaves: function (node, maxLevel) { + var leaves = [], levelsToShow = maxLevel || Number.MAX_VALUE; + this.each(node, function(elem, i) { + if(i < levelsToShow && + (!elem.children || elem.children.length == 0 )) { + leaves.push({ + 'node':elem, + 'level':levelsToShow - i + }); + } + }); + return leaves; + }, + + + /* + Method: eachLevel + + Iterates on tree nodes with relative depth less or equal than a specified level. + + Parameters: + + tree - A JSON tree or subtree. See also . + initLevel - An integer specifying the initial relative level. Usually zero. + toLevel - An integer specifying a top level. This method will iterate only through nodes with depth less than or equal this number. + action - A function that receives a node and an integer specifying the actual level of the node. + + Example: + (start code js) + TreeUtil.eachLevel(tree, 0, 3, function(node, depth) { + alert(node.name + ' ' + depth); + }); + (end code) + */ + eachLevel: function(tree, initLevel, toLevel, action) { + if(initLevel <= toLevel) { + action(tree, initLevel); + for(var i=0, ch = tree.children; i. + action - A function that receives a node. + + Example: + (start code js) + TreeUtil.each(tree, function(node) { + alert(node.name); + }); + (end code) + + */ + each: function(tree, action) { + this.eachLevel(tree, 0, Number.MAX_VALUE, action); + }, + + /* + Method: loadSubtrees + + Appends subtrees to leaves by requesting new subtrees + with the _request_ method. + + Parameters: + + tree - A JSON tree node. . + controller - An object that implements a request method. + + Example: + (start code js) + TreeUtil.loadSubtrees(leafNode, { + request: function(nodeId, level, onComplete) { + //Pseudo-code to make an ajax request for a new subtree + // that has as root id _nodeId_ and depth _level_ ... + Ajax.request({ + 'url': 'http://subtreerequesturl/', + + onSuccess: function(json) { + onComplete.onComplete(nodeId, json); + } + }); + } + }); + (end code) + */ + loadSubtrees: function(tree, controller) { + var maxLevel = controller.request && controller.levelsToShow; + var leaves = this.getLeaves(tree, maxLevel), + len = leaves.length, + selectedNode = {}; + if(len == 0) controller.onComplete(); + for(var i=0, counter=0; i, , + */ +/* + Class: Canvas + + A multi-purpose Canvas Class. This Class can be used with the ExCanvas library to provide + cross browser Canvas based visualizations. + + Parameters: + + id - The canvas id. This id will be used as prefix for the canvas widget DOM elements ids. + options - An object containing multiple options such as + + - _injectInto_ This property is _required_ and it specifies the id of the DOM element + to which the Canvas widget will be appended + - _width_ The width of the Canvas widget. Default's to 200px + - _height_ The height of the Canvas widget. Default's to 200px + - _backgroundColor_ Used for compatibility with IE. The canvas' background color. + Default's to '#333' + - _styles_ A hash containing canvas specific style properties such as _fillStyle_ and _strokeStyle_ among others. + + Example: + + Suppose we have this HTML + + (start code xml) +
+ (end code) + + Now we create a new Canvas instance + + (start code js) + //Create a new canvas instance + var canvas = new Canvas('mycanvas', { + //Where to inject the canvas. Any div container will do. + 'injectInto':'infovis', + //width and height for canvas. Default's to 200. + 'width': 900, + 'height':500, + //Canvas styles + 'styles': { + 'fillStyle': '#ccddee', + 'strokeStyle': '#772277' + } + }); + (end code) + + The generated HTML will look like this + + (start code xml) +
+
+ +
+
+
+
+ (end code) + + As you can see, the generated HTML consists of a canvas DOM element of id _mycanvas-canvas_ and a div label container + of id _mycanvas-label_, wrapped in a main div container of id _mycanvas_. + You can also add a background canvas, for making background drawings. + This is how the background concentric circles are drawn + + Example: + + (start code js) + //Create a new canvas instance. + var canvas = new Canvas('mycanvas', { + //Where to inject the canvas. Any div container will do. + 'injectInto':'infovis', + //width and height for canvas. Default's to 200. + 'width': 900, + 'height':500, + //Canvas styles + 'styles': { + 'fillStyle': '#ccddee', + 'strokeStyle': '#772277' + }, + //Add a background canvas for plotting + //concentric circles. + 'backgroundCanvas': { + //Add Canvas styles for the bck canvas. + 'styles': { + 'fillStyle': '#444', + 'strokeStyle': '#444' + }, + //Add the initialization and plotting functions. + 'impl': { + 'init': function() {}, + 'plot': function(canvas, ctx) { + var times = 6, d = 100; + var pi2 = Math.PI*2; + for(var i=1; i<=times; i++) { + ctx.beginPath(); + ctx.arc(0, 0, i * d, 0, pi2, true); + ctx.stroke(); + ctx.closePath(); + } + } + } + } + }); + (end code) + + The _backgroundCanvas_ object contains a canvas _styles_ property and + an _impl_ key to be used for implementing background canvas specific code. + + The _init_ method is only called once, at the instanciation of the background canvas. + The _plot_ method is called for plotting a Canvas image. + */ +this.Canvas = (function(){ + var config = { + 'injectInto': 'id', + + 'width': 200, + 'height': 200, + //deprecated + 'backgroundColor': '#333333', + + 'styles': { + 'fillStyle': '#000000', + 'strokeStyle': '#000000' + }, + + 'backgroundCanvas': false + }; + + function hasCanvas(){ + hasCanvas.t = hasCanvas.t || typeof(HTMLCanvasElement); + return "function" == hasCanvas.t || "object" == hasCanvas.t; + }; + + function create(tag, prop, styles){ + var elem = document.createElement(tag); + (function(obj, prop){ + if (prop) { + for (var p in prop) { + obj[p] = prop[p]; + } + } + return arguments.callee; + })(elem, prop)(elem.style, styles); + //feature check + if (tag == "canvas" && !hasCanvas() && G_vmlCanvasManager) { + elem = G_vmlCanvasManager.initElement(document.body.appendChild(elem)); + } + + return elem; + }; + + function get(id){ + return document.getElementById(id); + }; + + function translateToCenter(canvas, ctx, w, h){ + var width = w ? (canvas.width - w) : canvas.width; + var height = h ? (canvas.height - h) : canvas.height; + ctx.translate(width / 2, height / 2); + }; + + return function(id, opt){ + var ctx, bkctx, mainContainer, labelContainer, canvas, bkcanvas; + if (arguments.length < 1) + throw "Arguments missing"; + var idLabel = id + "-label", idCanvas = id + "-canvas", idBCanvas = id + "-bkcanvas"; + opt = $merge(config, opt || {}); + //create elements + var dim = { + 'width': opt.width, + 'height': opt.height + }; + mainContainer = create("div", { + 'id': id + }, $merge(dim, { + 'position': 'relative' + })); + labelContainer = create("div", { + 'id': idLabel + }, { + 'overflow': 'visible', + 'position': 'absolute', + 'top': 0, + 'left': 0, + 'width': dim.width + 'px', + 'height': 0 + }); + var dimPos = { + 'position': 'absolute', + 'top': 0, + 'left': 0, + 'width': dim.width + 'px', + 'height': dim.height + 'px' + }; + canvas = create("canvas", $merge({ + 'id': idCanvas + }, dim), dimPos); + var bc = opt.backgroundCanvas; + if (bc) { + bkcanvas = create("canvas", $merge({ + 'id': idBCanvas + }, dim), dimPos); + //append elements + mainContainer.appendChild(bkcanvas); + } + mainContainer.appendChild(canvas); + mainContainer.appendChild(labelContainer); + get(opt.injectInto).appendChild(mainContainer); + + //create contexts + ctx = canvas.getContext('2d'); + translateToCenter(canvas, ctx); + var st = opt.styles; + var s; + for (s in st) + ctx[s] = st[s]; + if (bc) { + bkctx = bkcanvas.getContext('2d'); + st = bc.styles; + for (s in st) { + bkctx[s] = st[s]; + } + translateToCenter(bkcanvas, bkctx); + bc.impl.init(bkcanvas, bkctx); + bc.impl.plot(bkcanvas, bkctx); + } + //create methods + return { + 'id': id, + /* + Method: getCtx + + Returns the main canvas context object + + Returns: + + Main canvas context + + Example: + + (start code js) + var ctx = canvas.getCtx(); + //Now I can use the native canvas context + //and for example change some canvas styles + ctx.globalAlpha = 1; + (end code) + */ + getCtx: function(){ + return ctx; + }, + + /* + Method: getElement + Returns the main Canvas DOM wrapper + + Returns: + DOM canvas wrapper generated, (i.e the div wrapper element with id _mycanvas_) + + Example: + (start code js) + var wrapper = canvas.getElement(); + //Returns
...
as element + (end code) + */ + getElement: function(){ + return mainContainer; + }, + + /* + Method: resize + + Resizes the canvas. + + Parameters: + + width - New canvas width. + height - New canvas height. + + This method can be used with the , or visualizations to resize + the visualizations + + Example: + + (start code js) + function resizeViz(width, height) { + canvas.resize(width, height); + rgraph.refresh(); //ht.refresh or st.refresh() also work. + rgraph.onAfterCompute(); + } + (end code) + + */ + resize: function(width, height){ + var pwidth = canvas.width, pheight = canvas.height; + canvas.width = width; + canvas.height = height; + canvas.style.width = width + "px"; + canvas.style.height = height + "px"; + if (bc) { + bkcanvas.width = width; + bkcanvas.height = height; + bkcanvas.style.width = width + "px"; + bkcanvas.style.height = height + "px"; + } + //small ExCanvas fix + if(!hasCanvas()) { + translateToCenter(canvas, ctx, pwidth, pheight); + } else { + translateToCenter(canvas, ctx); + } + + var st = opt.styles; + var s; + for (s in st) { + ctx[s] = st[s]; + } + if (bc) { + st = bc.styles; + for (s in st) + bkctx[s] = st[s]; + //same ExCanvas fix here + if(!hasCanvas()) { + translateToCenter(bkcanvas, bkctx, pwidth, pheight); + } else { + translateToCenter(bkcanvas, bkctx); + } + + bc.impl.init(bkcanvas, bkctx); + bc.impl.plot(bkcanvas, bkctx); + } + }, + + /* + Method: getSize + + Returns canvas dimensions. + + Returns: + + An object with _width_ and _height_ properties. + Example: + (start code js) + canvas.getSize(); //returns { width: 900, height: 500 } + (end code) + */ + getSize: function(){ + return { + 'width': canvas.width, + 'height': canvas.height + }; + }, + + path: function(type, action){ + ctx.beginPath(); + action(ctx); + ctx[type](); + ctx.closePath(); + }, + + /* + Method: clear + + Clears the canvas object. + */ + clear: function(){ + var size = this.getSize(); + ctx.clearRect(-size.width / 2, -size.height / 2, size.width, size.height); + }, + + /* + Method: clearReactangle + + Same as but only clears a section of the canvas. + + Parameters: + + top - An integer specifying the top of the rectangle. + right - An integer specifying the right of the rectangle. + bottom - An integer specifying the bottom of the rectangle. + left - An integer specifying the left of the rectangle. + */ + clearRectangle: function(top, right, bottom, left){ + //if using excanvas + if (!hasCanvas()) { + var f0 = ctx.fillStyle; + ctx.fillStyle = opt.backgroundColor; + ctx.fillRect(left, top, Math.abs(right - left), Math.abs(bottom - top)); + ctx.fillStyle = f0; + } + else { + ctx.clearRect(left, top, Math.abs(right - left), Math.abs(bottom - top)); + } + } + }; + }; + +})(); + + + +/* + * File: Polar.js + * + * Defines the class. + * + * Description: + * + * The class, just like the class, is used by the , and as a 2D point representation. + * + * See also: + * + * + * +*/ + +/* + Class: Polar + + A multi purpose polar representation. + + Description: + + The class, just like the class, is used by the , and as a 2D point representation. + + See also: + + + + Parameters: + + theta - An angle. + rho - The norm. +*/ + +this.Polar = function(theta, rho) { + this.theta = theta; + this.rho = rho; +}; + +Polar.prototype = { + /* + Method: getc + + Returns a complex number. + + Parameters: + + simple - _optional_ If *true*, this method will return only an object holding x and y properties and not a instance. Default's *false*. + + Returns: + + A complex number. + */ + getc: function(simple) { + return this.toComplex(simple); + }, + + /* + Method: getp + + Returns a representation. + + Returns: + + A variable in polar coordinates. + */ + getp: function() { + return this; + }, + + + /* + Method: set + + Sets a number. + + Parameters: + + v - A or instance. + + */ + set: function(v) { + v = v.getp(); + this.theta = v.theta; this.rho = v.rho; + }, + + /* + Method: setc + + Sets a number. + + Parameters: + + x - A number real part. + y - A number imaginary part. + + */ + setc: function(x, y) { + this.rho = Math.sqrt(x * x + y * y); + this.theta = Math.atan2(y, x); + if(this.theta < 0) this.theta += Math.PI * 2; + }, + + /* + Method: setp + + Sets a polar number. + + Parameters: + + theta - A number angle property. + rho - A number rho property. + + */ + setp: function(theta, rho) { + this.theta = theta; + this.rho = rho; + }, + + /* + Method: clone + + Returns a copy of the current object. + + Returns: + + A copy of the real object. + */ + clone: function() { + return new Polar(this.theta, this.rho); + }, + + /* + Method: toComplex + + Translates from polar to cartesian coordinates and returns a new instance. + + Parameters: + + simple - _optional_ If *true* this method will only return an object with x and y properties (and not the whole instance). Default's *false*. + + Returns: + + A new instance. + */ + toComplex: function(simple) { + var x = Math.cos(this.theta) * this.rho; + var y = Math.sin(this.theta) * this.rho; + if(simple) return { 'x': x, 'y': y}; + return new Complex(x, y); + }, + + /* + Method: add + + Adds two instances. + + Parameters: + + polar - A number. + + Returns: + + A new Polar instance. + */ + add: function(polar) { + return new Polar(this.theta + polar.theta, this.rho + polar.rho); + }, + + /* + Method: scale + + Scales a polar norm. + + Parameters: + + number - A scale factor. + + Returns: + + A new Polar instance. + */ + scale: function(number) { + return new Polar(this.theta, this.rho * number); + }, + + /* + Method: equals + + Comparison method. + + Returns *true* if the theta and rho properties are equal. + + Parameters: + + c - A number. + + Returns: + + *true* if the theta and rho parameters for these objects are equal. *false* otherwise. + */ + equals: function(c) { + return this.theta == c.theta && this.rho == c.rho; + }, + + /* + Method: $add + + Adds two instances affecting the current object. + + Paramters: + + polar - A instance. + + Returns: + + The changed object. + */ + $add: function(polar) { + this.theta = this.theta + polar.theta; this.rho += polar.rho; + return this; + }, + + /* + Method: $madd + + Adds two instances affecting the current object. The resulting theta angle is modulo 2pi. + + Parameters: + + polar - A instance. + + Returns: + + The changed object. + */ + $madd: function(polar) { + this.theta = (this.theta + polar.theta) % (Math.PI * 2); this.rho += polar.rho; + return this; + }, + + + /* + Method: $scale + + Scales a polar instance affecting the object. + + Parameters: + + number - A scaling factor. + + Returns: + + The changed object. + */ + $scale: function(number) { + this.rho *= number; + return this; + }, + + /* + Method: interpolate + + Calculates a polar interpolation between two points at a given delta moment. + + Parameters: + + elem - A instance. + delta - A delta factor ranging [0, 1]. + + Returns: + + A new instance representing an interpolation between _this_ and _elem_ + */ + interpolate: function(elem, delta) { + var pi = Math.PI, pi2 = pi * 2; + var ch = function(t) { + return (t < 0)? (t % pi2) + pi2 : t % pi2; + }; + var tt = this.theta, et = elem.theta; + var sum; + if(Math.abs(tt - et) > pi) { + if(tt > et) { + sum =ch((et + ((tt - pi2) - et) * delta)) ; + } else { + sum =ch((et - pi2 + (tt - (et - pi2)) * delta)); + } + } else { + sum =ch((et + (tt - et) * delta)) ; + } + var r = (this.rho - elem.rho) * delta + elem.rho; + return { + 'theta': sum, + 'rho': r + }; + } +}; + + +var $P = function(a, b) { return new Polar(a, b); }; + +Polar.KER = $P(0, 0); + + + +/* + * File: Complex.js + * + * Defines the class. + * + * Description: + * + * The class, just like the class, is used by the , and as a 2D point representation. + * + * See also: + * + * + * +*/ + +/* + Class: Complex + + A multi-purpose Complex Class with common methods. + + Description: + + The class, just like the class, is used by the , and as a 2D point representation. + + See also: + + + + Parameters: + + x - _optional_ A Complex number real part. + y - _optional_ A Complex number imaginary part. + +*/ + +this.Complex = function(x, y) { + this.x = x; + this.y = y; +}; + +Complex.prototype = { + /* + Method: getc + + Returns a complex number. + + Returns: + + A complex number. + */ + getc: function() { + return this; + }, + + /* + Method: getp + + Returns a representation of this number. + + Parameters: + + simple - _optional_ If *true*, this method will return only an object holding theta and rho properties and not a instance. Default's *false*. + + Returns: + + A variable in coordinates. + */ + getp: function(simple) { + return this.toPolar(simple); + }, + + + /* + Method: set + + Sets a number. + + Parameters: + + c - A or instance. + + */ + set: function(c) { + c = c.getc(true); + this.x = c.x; + this.y = c.y; + }, + + /* + Method: setc + + Sets a complex number. + + Parameters: + + x - A number Real part. + y - A number Imaginary part. + + */ + setc: function(x, y) { + this.x = x; + this.y = y; + }, + + /* + Method: setp + + Sets a polar number. + + Parameters: + + theta - A number theta property. + rho - A number rho property. + + */ + setp: function(theta, rho) { + this.x = Math.cos(theta) * rho; + this.y = Math.sin(theta) * rho; + }, + + /* + Method: clone + + Returns a copy of the current object. + + Returns: + + A copy of the real object. + */ + clone: function() { + return new Complex(this.x, this.y); + }, + + /* + Method: toPolar + + Transforms cartesian to polar coordinates. + + Parameters: + + simple - _optional_ If *true* this method will only return an object with theta and rho properties (and not the whole instance). Default's *false*. + + Returns: + + A new instance. + */ + + toPolar: function(simple) { + var rho = this.norm(); + var atan = Math.atan2(this.y, this.x); + if(atan < 0) atan += Math.PI * 2; + if(simple) return { 'theta': atan, 'rho': rho }; + return new Polar(atan, rho); + }, + /* + Method: norm + + Calculates a number norm. + + Returns: + + A real number representing the complex norm. + */ + norm: function () { + return Math.sqrt(this.squaredNorm()); + }, + + /* + Method: squaredNorm + + Calculates a number squared norm. + + Returns: + + A real number representing the complex squared norm. + */ + squaredNorm: function () { + return this.x*this.x + this.y*this.y; + }, + + /* + Method: add + + Returns the result of adding two complex numbers. + + Does not alter the original object. + + Parameters: + + pos - A instance. + + Returns: + + The result of adding two complex numbers. + */ + add: function(pos) { + return new Complex(this.x + pos.x, this.y + pos.y); + }, + + /* + Method: prod + + Returns the result of multiplying two numbers. + + Does not alter the original object. + + Parameters: + + pos - A instance. + + Returns: + + The result of multiplying two complex numbers. + */ + prod: function(pos) { + return new Complex(this.x*pos.x - this.y*pos.y, this.y*pos.x + this.x*pos.y); + }, + + /* + Method: conjugate + + Returns the conjugate of this number. + + Does not alter the original object. + + Returns: + + The conjugate of this number. + */ + conjugate: function() { + return new Complex(this.x, -this.y); + }, + + + /* + Method: scale + + Returns the result of scaling a instance. + + Does not alter the original object. + + Parameters: + + factor - A scale factor. + + Returns: + + The result of scaling this complex to a factor. + */ + scale: function(factor) { + return new Complex(this.x * factor, this.y * factor); + }, + + /* + Method: equals + + Comparison method. + + Returns *true* if both real and imaginary parts are equal. + + Parameters: + + c - A instance. + + Returns: + + A boolean instance indicating if both numbers are equal. + */ + equals: function(c) { + return this.x == c.x && this.y == c.y; + }, + + /* + Method: $add + + Returns the result of adding two numbers. + + Alters the original object. + + Parameters: + + pos - A instance. + + Returns: + + The result of adding two complex numbers. + */ + $add: function(pos) { + this.x += pos.x; this.y += pos.y; + return this; + }, + + /* + Method: $prod + + Returns the result of multiplying two numbers. + + Alters the original object. + + Parameters: + + pos - A instance. + + Returns: + + The result of multiplying two complex numbers. + */ + $prod:function(pos) { + var x = this.x, y = this.y; + this.x = x*pos.x - y*pos.y; + this.y = y*pos.x + x*pos.y; + return this; + }, + + /* + Method: $conjugate + + Returns the conjugate for this . + + Alters the original object. + + Returns: + + The conjugate for this complex. + */ + $conjugate: function() { + this.y = -this.y; + return this; + }, + + /* + Method: $scale + + Returns the result of scaling a instance. + + Alters the original object. + + Parameters: + + factor - A scale factor. + + Returns: + + The result of scaling this complex to a factor. + */ + $scale: function(factor) { + this.x *= factor; this.y *= factor; + return this; + }, + + /* + Method: $div + + Returns the division of two numbers. + + Alters the original object. + + Parameters: + + pos - A number. + + Returns: + + The result of scaling this complex to a factor. + */ + $div: function(pos) { + var x = this.x, y = this.y; + var sq = pos.squaredNorm(); + this.x = x * pos.x + y * pos.y; this.y = y * pos.x - x * pos.y; + return this.$scale(1 / sq); + } +}; + +var $C = function(a, b) { return new Complex(a, b); }; + +Complex.KER = $C(0, 0); + + + +/* + * File: Graph.js + * + * Generic , and classes. + * + * Used by: + * + * , and . + * +*/ + +/* + Class: Graph + + A generic Graph class. + + Description: + + When a json graph/tree structure is loaded by , an internal representation is created. + + In most cases you'll be dealing with an already created structure, so methods like or won't + be of many use. However methods like and are pretty useful. + + provides also iterators for and advanced and useful graph operations and methods. + + Used by: + + , , and . + + Access: + + An instance of this class can be accessed by using the _graph_ parameter of a , or instance + + Example: + + (start code js) + var st = new ST(canvas, config); + st.graph.getNode //or any other method. + + var ht = new Hypertree(canvas, config); + ht.graph.getNode //or any other method. + + var rg = new RGraph(canvas, config); + rg.graph.getNode //or any other method. + (end code) + +*/ + +this.Graph = new Class({ + + initialize: function(opt) { + var innerOptions = { + 'complex': false, + 'Node': {} + }; + this.opt = $merge(innerOptions, opt || {}); + this.nodes= {}; + }, + +/* + Method: getNode + + Returns a by _id_. + + Parameters: + + id - A id. + + Returns: + + A having _id_ as id. Returns *false* otherwise. + + Example: + + (start code js) + var node = graph.getNode('someid'); + (end code) +*/ + getNode: function(id) { + if(this.hasNode(id)) return this.nodes[id]; + return false; + }, + +/* + Method: getAdjacence + + Returns an array of objects connecting nodes with ids _id_ and _id2_. + + Parameters: + + id - A id. + id2 - A id. + + Returns: + + An Array of objects. Returns *false* if there's not a connecting those two nodes. +*/ + getAdjacence: function (id, id2) { + var adjs = []; + if(this.hasNode(id) && this.hasNode(id2) + && this.nodes[id].adjacentTo({ 'id':id2 }) && this.nodes[id2].adjacentTo({ 'id':id })) { + adjs.push(this.nodes[id].getAdjacency(id2)); + adjs.push(this.nodes[id2].getAdjacency(id)); + return adjs; + } + return false; + }, + + /* + Method: addNode + + Adds a node. + + Parameters: + + obj - An object containing as properties + + - _id_ node's id + - _name_ node's name + - _data_ node's data hash + + See also: + + + */ + addNode: function(obj) { + if(!this.nodes[obj.id]) { + this.nodes[obj.id] = new Graph.Node($extend({ + 'id': obj.id, + 'name': obj.name, + 'data': obj.data + }, this.opt.Node), this.opt.complex); + } + return this.nodes[obj.id]; + }, + + /* + Method: addAdjacence + + Connects nodes specified by _obj_ and _obj2_. If not found, nodes are created. + + Parameters: + + obj - a object. + obj2 - Another object. + data - A DataSet object. Used to store some extra information in the object created. + + See also: + + , + */ + addAdjacence: function (obj, obj2, data) { + var adjs = []; + if(!this.hasNode(obj.id)) { this.addNode(obj); } + if(!this.hasNode(obj2.id)) { this.addNode(obj2); } + obj = this.nodes[obj.id]; obj2 = this.nodes[obj2.id]; + + for(var i in this.nodes) { + if(this.nodes[i].id == obj.id) { + if(!this.nodes[i].adjacentTo(obj2)) { + adjs.push(this.nodes[i].addAdjacency(obj2, data)); + } + } + + if(this.nodes[i].id == obj2.id) { + if(!this.nodes[i].adjacentTo(obj)) { + adjs.push(this.nodes[i].addAdjacency(obj, data)); + } + } + } + return adjs; + }, + + /* + Method: removeNode + + Removes a matching the specified _id_. + + Parameters: + + id - A node's id. + + */ + removeNode: function(id) { + if(this.hasNode(id)) { + var node = this.nodes[id]; + for(var i=0 in node.adjacencies) { + var adj = node.adjacencies[i]; + this.removeAdjacence(id, adj.nodeTo.id); + } + delete this.nodes[id]; + } + }, + +/* + Method: removeAdjacence + + Removes a matching _id1_ and _id2_. + + Parameters: + + id1 - A id. + id2 - A id. +*/ + removeAdjacence: function(id1, id2) { + if(this.hasNode(id1)) this.nodes[id1].removeAdjacency(id2); + if(this.hasNode(id2)) this.nodes[id2].removeAdjacency(id1); + }, + + /* + Method: hasNode + + Returns a Boolean instance indicating if the node belongs to the or not. + + Parameters: + + id - Node id. + + Returns: + + A Boolean instance indicating if the node belongs to the graph or not. + */ + hasNode: function(id) { + return id in this.nodes; + } +}); + +/* + Class: Graph.Node + + A node. + + Parameters: + + obj - An object containing an 'id', 'name' and 'data' properties as described in . + complex - Whether node position properties should contain or instances. + + See also: + + + + Description: + + An instance of is usually passed as parameter for most configuration/controller methods in the + , and classes. + + A object has as properties + + id - Node id. + name - Node name. + data - Node data property containing a hash (i.e {}) with custom options. For more information see . + selected - Whether the node is selected or not. Used by for selecting nodes that are between the root node and the selected node. + angleSpan - For radial layouts such as the ones performed by the and the . Contains _begin_ and _end_ properties containing angle values describing the angle span for this subtree. + alpha - Current opacity value. + startAlpha - Opacity begin value. Used for interpolation. + endAlpha - Opacity end value. Used for interpolation. + pos - Current position. Can be a or instance. + startPos - Starting position. Used for interpolation. + endPos - Ending position. Used for interpolation. +*/ +Graph.Node = new Class({ + + initialize: function(opt, complex) { + var innerOptions = { + 'id': '', + 'name': '', + 'data': {}, + 'adjacencies': {}, + + 'selected': false, + 'drawn': false, + 'exist': false, + + 'angleSpan': { + 'begin': 0, + 'end' : 0 + }, + + 'alpha': 1, + 'startAlpha': 1, + 'endAlpha': 1, + + 'pos': (complex && $C(0, 0)) || $P(0, 0), + 'startPos': (complex && $C(0, 0)) || $P(0, 0), + 'endPos': (complex && $C(0, 0)) || $P(0, 0) + }; + + $extend(this, $extend(innerOptions, opt)); + }, + + /* + Method: adjacentTo + + Indicates if the node is adjacent to the node specified by id + + Parameters: + + id - A node id. + + Returns: + + A Boolean instance indicating whether this node is adjacent to the specified by id or not. + + Example: + (start code js) + node.adjacentTo('mynodeid'); + (end code) + */ + adjacentTo: function(node) { + return node.id in this.adjacencies; + }, + + /* + Method: getAdjacency + + Returns a object connecting the current and the node having _id_ as id. + + Parameters: + + id - A node id. + + Returns: + + A object or undefined. + */ + getAdjacency: function(id) { + return this.adjacencies[id]; + }, + /* + Method: addAdjacency + + Connects the current node and the given node. + + Parameters: + + node - A . + data - Some custom hash information. + */ + addAdjacency: function(node, data) { + var adj = new Graph.Adjacence(this, node, data); + return this.adjacencies[node.id] = adj; + }, + + /* + Method: removeAdjacency + + Removes a by _id_. + + Parameters: + + id - A node id. + */ + removeAdjacency: function(id) { + delete this.adjacencies[id]; + } +}); + +/* + Class: Graph.Adjacence + + A adjacence (or edge). Connects two . + + Parameters: + + nodeFrom - A . + nodeTo - A . + data - Some custom hash data. + + See also: + + + + Description: + + An instance of is usually passed as parameter for some configuration/controller methods in the + , and classes. + + A object has as properties + + nodeFrom - A connected by this edge. + nodeTo - Another connected by this edge. + data - Node data property containing a hash (i.e {}) with custom options. For more information see . + alpha - Current opacity value. + startAlpha - Opacity begin value. Used for interpolation. + endAlpha - Opacity end value. Used for interpolation. +*/ +Graph.Adjacence = function(nodeFrom, nodeTo, data) { + this.nodeFrom = nodeFrom; + this.nodeTo = nodeTo; + this.data = data || {}; + this.alpha = 1; + this.startAlpha = 1; + this.endAlpha = 1; +}; + +/* + Object: Graph.Util + + traversal and processing utility object. +*/ +Graph.Util = { + /* + filter + + For internal use only. Provides a filtering function based on flags. + */ + filter: function(param) { + if(!param || !($type(param) == 'string')) return function() { return true; }; + var props = param.split(" "); + return function(elem) { + for(var i=0; i by _id_. + + Parameters: + + graph - A instance. + id - A id. + + Returns: + + A node. + + Example: + + (start code js) + Graph.Util.getNode(graph, 'nodeid'); + (end code) + */ + getNode: function(graph, id) { + return graph.getNode(id); + }, + + /* + Method: eachNode + + Iterates over nodes performing an _action_. + + Parameters: + + graph - A instance. + action - A callback function having a as first formal parameter. + + Example: + (start code js) + Graph.Util.each(graph, function(node) { + alert(node.name); + }); + (end code) + */ + eachNode: function(graph, action, flags) { + var filter = this.filter(flags); + for(var i in graph.nodes) { + if(filter(graph.nodes[i])) action(graph.nodes[i]); + } + }, + + /* + Method: eachAdjacency + + Iterates over adjacencies applying the _action_ function. + + Parameters: + + node - A . + action - A callback function having as first formal parameter. + + Example: + (start code js) + Graph.Util.eachAdjacency(node, function(adj) { + alert(adj.nodeTo.name); + }); + (end code) + */ + eachAdjacency: function(node, action, flags) { + var adj = node.adjacencies, filter = this.filter(flags); + for(var id in adj) { + if(filter(adj[id])) { + action(adj[id], id); + } + } + }, + + /* + Method: computeLevels + + Performs a BFS traversal setting the correct depth for each node. + + The depth of each node can then be accessed by + >node._depth + + Parameters: + + graph - A . + id - A starting node id for the BFS traversal. + startDepth - _optional_ A minimum depth value. Default's 0. + + */ + computeLevels: function(graph, id, startDepth, flags) { + startDepth = startDepth || 0; + var filter = this.filter(flags); + this.eachNode(graph, function(elem) { + elem._flag = false; + elem._depth = -1; + }, flags); + var root = graph.getNode(id); + root._depth = startDepth; + var queue = [root]; + while(queue.length != 0) { + var node = queue.pop(); + node._flag = true; + this.eachAdjacency(node, function(adj) { + var n = adj.nodeTo; + if(n._flag == false && filter(n)) { + if(n._depth < 0) n._depth = node._depth + 1 + startDepth; + queue.unshift(n); + } + }, flags); + } + }, + + /* + Method: eachBFS + + Performs a BFS traversal applying _action_ to each . + + Parameters: + + graph - A . + id - A starting node id for the BFS traversal. + action - A callback function having a as first formal parameter. + + Example: + (start code js) + Graph.Util.eachBFS(graph, 'mynodeid', function(node) { + alert(node.name); + }); + (end code) + */ + eachBFS: function(graph, id, action, flags) { + var filter = this.filter(flags); + this.clean(graph); + var queue = [graph.getNode(id)]; + while(queue.length != 0) { + var node = queue.pop(); + node._flag = true; + action(node, node._depth); + this.eachAdjacency(node, function(adj) { + var n = adj.nodeTo; + if(n._flag == false && filter(n)) { + n._flag = true; + queue.unshift(n); + } + }, flags); + } + }, + + /* + Method: eachLevel + + Iterates over a node's subgraph applying _action_ to the nodes of relative depth between _levelBegin_ and _levelEnd_. + + Parameters: + + node - A . + levelBegin - A relative level value. + levelEnd - A relative level value. + action - A callback function having a as first formal parameter. + + */ + eachLevel: function(node, levelBegin, levelEnd, action, flags) { + var d = node._depth, filter = this.filter(flags), that = this; + levelEnd = levelEnd === false? Number.MAX_VALUE -d : levelEnd; + (function loopLevel(node, levelBegin, levelEnd) { + var d = node._depth; + if(d >= levelBegin && d <= levelEnd && filter(node)) action(node, d); + if(d < levelEnd) { + that.eachAdjacency(node, function(adj) { + var n = adj.nodeTo; + if(n._depth > d) loopLevel(n, levelBegin, levelEnd); + }); + } + })(node, levelBegin + d, levelEnd + d); + }, + + /* + Method: eachSubgraph + + Iterates over a node's children recursively. + + Parameters: + node - A . + action - A callback function having a as first formal parameter. + + Example: + (start code js) + Graph.Util.eachSubgraph(node, function(node) { + alert(node.name); + }); + (end code) + */ + eachSubgraph: function(node, action, flags) { + this.eachLevel(node, 0, false, action, flags); + }, + + /* + Method: eachSubnode + + Iterates over a node's children (without deeper recursion). + + Parameters: + node - A . + action - A callback function having a as first formal parameter. + + Example: + (start code js) + Graph.Util.eachSubnode(node, function(node) { + alert(node.name); + }); + (end code) + */ + eachSubnode: function(node, action, flags) { + this.eachLevel(node, 1, 1, action, flags); + }, + + /* + Method: anySubnode + + Returns *true* if any subnode matches the given condition. + + Parameters: + node - A . + cond - A callback function returning a Boolean instance. This function has as first formal parameter a . + + Returns: + A boolean value. + + Example: + (start code js) + Graph.Util.anySubnode(node, function(node) { return node.name == "mynodename"; }); + (end code) + */ + anySubnode: function(node, cond, flags) { + var flag = false; + cond = cond || $lambda(true); + var c = $type(cond) == 'string'? function(n) { return n[cond]; } : cond; + this.eachSubnode(node, function(elem) { + if(c(elem)) flag = true; + }, flags); + return flag; + }, + + /* + Method: getSubnodes + + Collects all subnodes for a specified node. The _level_ parameter filters nodes having relative depth of _level_ from the root node. + + Parameters: + node - A . + level - _optional_ A starting relative depth for collecting nodes. Default's 0. + + Returns: + An array of nodes. + + */ + getSubnodes: function(node, level, flags) { + var ans = [], that = this; + level = level || 0; + var levelStart, levelEnd; + if($type(level) == 'array') { + levelStart = level[0]; + levelEnd = level[1]; + } else { + levelStart = level; + levelEnd = Number.MAX_VALUE - node._depth; + } + this.eachLevel(node, levelStart, levelEnd, function(n) { + ans.push(n); + }, flags); + return ans; + }, + + + /* + Method: getParents + + Returns an Array of wich are parents of the given node. + + Parameters: + node - A . + + Returns: + An Array of . + + Example: + (start code js) + var pars = Graph.Util.getParents(node); + if(pars.length > 0) { + //do stuff with parents + } + (end code) + */ + getParents: function(node) { + var ans = []; + this.eachAdjacency(node, function(adj) { + var n = adj.nodeTo; + if(n._depth < node._depth) ans.push(n); + }); + return ans; + }, + + /* + Method: isDescendantOf + + Returns a Boolean instance indicating if some node is descendant of the node with the given id. + + Parameters: + node - A . + id - A id. + + Returns: + Ture if _node_ is descendant of the node with the given _id_. False otherwise. + + Example: + (start code js) + var pars = Graph.Util.isDescendantOf(node, "nodeid"); + (end code) + */ + isDescendantOf: function(node, id) { + if(node.id == id) return true; + var pars = this.getParents(node), ans = false; + for ( var i = 0; !ans && i < pars.length; i++) { + ans = ans || this.isDescendantOf(pars[i], id); + } + return ans; + }, + + /* + Method: clean + + Cleans flags from nodes (by setting the _flag_ property to false). + + Parameters: + graph - A instance. + */ + clean: function(graph) { this.eachNode(graph, function(elem) { elem._flag = false; }); } +}; + + + +/* + * File: Graph.Op.js + * + * Defines an abstract class for performing Operations. +*/ + +/* + Object: Graph.Op + + Generic Operations. + + Description: + + An abstract class holding unary and binary powerful graph operations such as removingNodes, removingEdges, adding two graphs and morphing. + + Implemented by: + + , and . + + Access: + + The subclasses for this abstract class can be accessed by using the _op_ property of the , or instances created. + + See also: + + , , , , , , . +*/ +Graph.Op = { + + options: { + type: 'nothing', + duration: 2000, + hideLabels: true, + fps:30 + }, + + /* + Method: removeNode + + Removes one or more from the visualization. + It can also perform several animations like fading sequentially, fading concurrently, iterating or replotting. + + Parameters: + + node - The node's id. Can also be an array having many ids. + opt - Animation options. It's an object with optional properties + + - _type_ Type of the animation. Can be "nothing", "replot", "fade:seq", "fade:con" or "iter". Default's "nothing". + - _duration_ Duration of the animation in milliseconds. Default's 2000. + - _fps_ Frames per second for the animation. Default's 30. + - _hideLabels_ Hide labels during the animation. Default's *true*. + - _transition_ Transitions defined in the class. Default's the default transition option of the + , or instance created. + + Example: + (start code js) + var rg = new RGraph(canvas, config); //could be new ST or new Hypertree also. + rg.op.removeNode('nodeid', { + type: 'fade:seq', + duration: 1000, + hideLabels: false, + transition: Trans.Quart.easeOut + }); + //or also + rg.op.removeNode(['someid', 'otherid'], { + type: 'fade:con', + duration: 1500 + }); + (end code) + */ + + removeNode: function(node, opt) { + var viz = this.viz; + var options = $merge(this.options, viz.controller, opt); + var n = $splat(node); + var i, that, nodeObj; + switch(options.type) { + case 'nothing': + for(i=0; i class. Default's the default transition option of the + , or instance created. + + Example: + (start code js) + var rg = new RGraph(canvas, config); //could be new ST or new Hypertree also. + rg.op.removeEdge(['nodeid', 'otherid'], { + type: 'fade:seq', + duration: 1000, + hideLabels: false, + transition: Trans.Quart.easeOut + }); + //or also + rg.op.removeEdge([['someid', 'otherid'], ['id3', 'id4']], { + type: 'fade:con', + duration: 1500 + }); + (end code) + + */ + removeEdge: function(vertex, opt) { + var viz = this.viz; + var options = $merge(this.options, viz.controller, opt); + var v = ($type(vertex[0]) == 'string')? [vertex] : vertex; + var i, that, adjs; + switch(options.type) { + case 'nothing': + for(i=0; i + + Parameters: + + json - A json tree or graph structure. See also . + opt - Animation options. It's an object with optional properties + + - _type_ Type of the animation. Can be "nothing", "replot", "fade:seq" or "fade:con". Default's "nothing". + - _duration_ Duration of the animation in milliseconds. Default's 2000. + - _fps_ Frames per second for the animation. Default's 30. + - _hideLabels_ Hide labels during the animation. Default's *true*. + - _transition_ Transitions defined in the class. Default's the default transition option of the + , or instance created. + + Example: + (start code js) + //json contains a tree or graph structure. + + var rg = new RGraph(canvas, config); //could be new ST or new Hypertree also. + rg.op.sum(json, { + type: 'fade:seq', + duration: 1000, + hideLabels: false, + transition: Trans.Quart.easeOut + }); + //or also + rg.op.sum(json, { + type: 'fade:con', + duration: 1500 + }); + (end code) + + */ + sum: function(json, opt) { + var viz = this.viz; + var options = $merge(this.options, viz.controller, opt), root = viz.root; + var GUtil, graph; + viz.root = opt.id || viz.root; + switch(options.type) { + case 'nothing': + graph = viz.construct(json); + GUtil = Graph.Util; + GUtil.eachNode(graph, function(elem) { + GUtil.eachAdjacency(elem, function(adj) { + viz.graph.addAdjacence(adj.nodeFrom, adj.nodeTo, adj.data); + }); + }); + break; + + case 'replot': + viz.refresh(true); + this.sum(json, { type: 'nothing' }); + viz.refresh(true); + break; + + case 'fade:seq': case 'fade': case 'fade:con': + GUtil = Graph.Util; + that = this; + graph = viz.construct(json); + + //set alpha to 0 for nodes to add. + var fadeEdges = this.preprocessSum(graph); + var modes = !fadeEdges? ['fade:nodes'] : ['fade:nodes', 'fade:vertex']; + viz.reposition(); + if(options.type != 'fade:con') { + viz.fx.animate($merge(options, { + modes: ['linear'], + onComplete: function() { + viz.fx.animate($merge(options, { + modes: modes, + onComplete: function() { + options.onComplete(); + } + })); + } + })); + } else { + GUtil.eachNode(viz.graph, function(elem) { + if (elem.id != root && elem.pos.getp().equals(Polar.KER)) { + elem.pos.set(elem.endPos); elem.startPos.set(elem.endPos); + } + }); + viz.fx.animate($merge(options, { + modes: ['linear'].concat(modes) + })); + } + break; + + default: this.doError(); + } + }, + + /* + Method: morph + + This method will _morph_ the current visualized graph into the new _json_ representation passed in the method. + + Can also perform multiple animations. The _json_ object must at least have the root node in common with the current visualized graph. + + Parameters: + + json - A json tree or graph structure. See also . + opt - Animation options. It's an object with optional properties + + - _type_ Type of the animation. Can be "nothing", "replot", or "fade". Default's "nothing". + - _duration_ Duration of the animation in milliseconds. Default's 2000. + - _fps_ Frames per second for the animation. Default's 30. + - _hideLabels_ Hide labels during the animation. Default's *true*. + - _transition_ Transitions defined in the class. Default's the default transition option of the + , or instance created. + + Example: + (start code js) + //json contains a tree or graph structure. + + var rg = new RGraph(canvas, config); //could be new ST or new Hypertree also. + rg.op.morph(json, { + type: 'fade', + duration: 1000, + hideLabels: false, + transition: Trans.Quart.easeOut + }); + //or also + rg.op.morph(json, { + type: 'fade', + duration: 1500 + }); + (end code) + + */ + morph: function(json, opt) { + var viz = this.viz; + var options = $merge(this.options, viz.controller, opt), root = viz.root; + var GUtil, graph; + viz.root = opt.id || viz.root; + switch(options.type) { + case 'nothing': + graph = viz.construct(json); + GUtil = Graph.Util; + GUtil.eachNode(graph, function(elem) { + GUtil.eachAdjacency(elem, function(adj) { + viz.graph.addAdjacence(adj.nodeFrom, adj.nodeTo, adj.data); + }); + }); + GUtil.eachNode(viz.graph, function(elem) { + GUtil.eachAdjacency(elem, function(adj) { + if(!graph.getAdjacence(adj.nodeFrom.id, adj.nodeTo.id)) { + viz.graph.removeAdjacence(adj.nodeFrom.id, adj.nodeTo.id); + } + }); + if(!graph.hasNode(elem.id)) viz.graph.removeNode(elem.id); + }); + + break; + + case 'replot': + viz.fx.clearLabels(true); + this.morph(json, { type: 'nothing' }); + viz.refresh(true); + viz.refresh(true); + break; + + case 'fade:seq': case 'fade': case 'fade:con': + GUtil = Graph.Util; + that = this; + graph = viz.construct(json); + //preprocessing for adding nodes. + var fadeEdges = this.preprocessSum(graph); + //preprocessing for nodes to delete. + GUtil.eachNode(viz.graph, function(elem) { + if(!graph.hasNode(elem.id)) { + elem.alpha = 1; elem.startAlpha = 1; elem.endAlpha = 0; elem.ignore = true; + } + }); + GUtil.eachNode(viz.graph, function(elem) { + if(elem.ignore) return; + GUtil.eachAdjacency(elem, function(adj) { + if(adj.nodeFrom.ignore || adj.nodeTo.ignore) return; + var nodeFrom = graph.getNode(adj.nodeFrom.id); + var nodeTo = graph.getNode(adj.nodeTo.id); + if(!nodeFrom.adjacentTo(nodeTo)) { + var adjs = viz.graph.getAdjacence(nodeFrom.id, nodeTo.id); + fadeEdges = true; + adjs[0].alpha = 1; adjs[0].startAlpha = 1; adjs[0].endAlpha = 0; adjs[0].ignore = true; + adjs[1].alpha = 1; adjs[1].startAlpha = 1; adjs[1].endAlpha = 0; adjs[1].ignore = true; + } + }); + }); + var modes = !fadeEdges? ['fade:nodes'] : ['fade:nodes', 'fade:vertex']; + viz.reposition(); + GUtil.eachNode(viz.graph, function(elem) { + if (elem.id != root && elem.pos.getp().equals(Polar.KER)) { + elem.pos.set(elem.endPos); elem.startPos.set(elem.endPos); + } + }); + viz.fx.animate($merge(options, { + modes: ['polar'].concat(modes), + onComplete: function() { + GUtil.eachNode(viz.graph, function(elem) { + if(elem.ignore) viz.graph.removeNode(elem.id); + }); + GUtil.eachNode(viz.graph, function(elem) { + GUtil.eachAdjacency(elem, function(adj) { + if(adj.ignore) viz.graph.removeAdjacence(adj.nodeFrom.id, adj.nodeTo.id); + }); + }); + options.onComplete(); + } + })); + break; + + default: this.doError(); + } + }, + + preprocessSum: function(graph) { + var viz = this.viz; + var GUtil = Graph.Util; + GUtil.eachNode(graph, function(elem) { + if(!viz.graph.hasNode(elem.id)) { + viz.graph.addNode(elem); + var n = viz.graph.getNode(elem.id); + n.alpha = 0; n.startAlpha = 0; n.endAlpha = 1; + } + }); + var fadeEdges = false; + GUtil.eachNode(graph, function(elem) { + GUtil.eachAdjacency(elem, function(adj) { + var nodeFrom = viz.graph.getNode(adj.nodeFrom.id); + var nodeTo = viz.graph.getNode(adj.nodeTo.id); + if(!nodeFrom.adjacentTo(nodeTo)) { + var adjs = viz.graph.addAdjacence(nodeFrom, nodeTo, adj.data); + if(nodeFrom.startAlpha == nodeFrom.endAlpha + && nodeTo.startAlpha == nodeTo.endAlpha) { + fadeEdges = true; + adjs[0].alpha = 0; adjs[0].startAlpha = 0; adjs[0].endAlpha = 1; + adjs[1].alpha = 0; adjs[1].startAlpha = 0; adjs[1].endAlpha = 1; + } + } + }); + }); + return fadeEdges; + } +}; + + + +/* + * File: Graph.Plot.js + * + * Defines an abstract class for performing rendering and animation. + * + */ + + +/* + Object: Graph.Plot + + Generic rendering and animation methods. + + Description: + + An abstract class for plotting a generic graph structure. + + Implemented by: + + , , . + + Access: + + The subclasses for this abstract class can be accessed by using the _fx_ property of the , , or instances created. + + See also: + + , , , , , , . + +*/ +Graph.Plot = { + + Interpolator: { + 'moebius': function(elem, delta, vector) { + if(delta <= 1 || vector.norm() <= 1) { + var x = vector.x, y = vector.y; + var ans = elem.startPos.getc().moebiusTransformation(vector); + elem.pos.setc(ans.x, ans.y); + vector.x = x; vector.y = y; + } + }, + + 'linear': function(elem, delta) { + var from = elem.startPos.getc(true); + var to = elem.endPos.getc(true); + elem.pos.setc((to.x - from.x) * delta + from.x, (to.y - from.y) * delta + from.y); + }, + + 'fade:nodes': function(elem, delta) { + if(delta <= 1 && (elem.endAlpha != elem.alpha)) { + var from = elem.startAlpha; + var to = elem.endAlpha; + elem.alpha = from + (to - from) * delta; + } + }, + + 'fade:vertex': function(elem, delta) { + var adjs = elem.adjacencies; + for(var id in adjs) this['fade:nodes'](adjs[id], delta); + }, + + 'polar': function(elem, delta) { + var from = elem.startPos.getp(true); + var to = elem.endPos.getp(); + var ans = to.interpolate(from, delta); + elem.pos.setp(ans.theta, ans.rho); + } + }, + + //A flag value indicating if node labels are being displayed or not. + labelsHidden: false, + //Label DOM element + labelContainer: false, + //Label DOM elements hash. + labels: {}, + + /* + Method: getLabelContainer + + Lazy fetcher for the label container. + + Returns: + + The label container DOM element. + + Example: + + (start code js) + var rg = new RGraph(canvas, config); //can be also Hypertree or ST + var labelContainer = rg.fx.getLabelContainer(); + alert(labelContainer.innerHTML); + (end code) + */ + getLabelContainer: function() { + return this.labelContainer? this.labelContainer : this.labelContainer = document.getElementById(this.viz.config.labelContainer); + }, + + /* + Method: getLabel + + Lazy fetcher for the label DOM element. + + Parameters: + + id - The label id (which is also a id). + + Returns: + + The label DOM element. + + Example: + + (start code js) + var rg = new RGraph(canvas, config); //can be also Hypertree or ST + var label = rg.fx.getLabel('someid'); + alert(label.innerHTML); + (end code) + + */ + getLabel: function(id) { + return (id in this.labels && this.labels[id] != null)? this.labels[id] : this.labels[id] = document.getElementById(id); + }, + + /* + Method: hideLabels + + Hides all labels (by hiding the label container). + + Parameters: + + hide - A boolean value indicating if the label container must be hidden or not. + + Example: + (start code js) + var rg = new RGraph(canvas, config); //can be also Hypertree or ST + rg.fx.hideLabels(true); + (end code) + + */ + hideLabels: function (hide) { + var container = this.getLabelContainer(); + if(hide) container.style.display = 'none'; + else container.style.display = ''; + this.labelsHidden = hide; + }, + + /* + Method: clearLabels + + Clears the label container. + + Useful when using a new visualization with the same canvas element/widget. + + Parameters: + + force - Forces deletion of all labels. + + Example: + (start code js) + var rg = new RGraph(canvas, config); //can be also Hypertree or ST + rg.fx.clearLabels(); + (end code) + */ + clearLabels: function(force) { + for(var id in this.labels) { + if (force || !this.viz.graph.hasNode(id)) { + this.disposeLabel(id); + delete this.labels[id]; + } + } + }, + + /* + Method: disposeLabel + + Removes a label. + + Parameters: + + id - A label id (which generally is also a id). + + Example: + (start code js) + var rg = new RGraph(canvas, config); //can be also Hypertree or ST + rg.fx.disposeLabel('labelid'); + (end code) + */ + disposeLabel: function(id) { + var elem = this.getLabel(id); + if(elem && elem.parentNode) { + elem.parentNode.removeChild(elem); + } + }, + + /* + Method: hideLabel + + Hides the corresponding label. + + Parameters: + + node - A . Can also be an array of . + flag - If *true*, nodes will be shown. Otherwise nodes will be hidden. + + Example: + (start code js) + var rg = new RGraph(canvas, config); //can be also Hypertree or ST + rg.fx.hideLabel(rg.graph.getNode('someid'), false); + (end code) + */ + hideLabel: function(node, flag) { + node = $splat(node); + var st = flag? "" : "none", lab, that = this; + $each(node, function(n) { + var lab = that.getLabel(n.id); + if (lab) { + lab.style.display = st; + } + }); + }, + + /* + Method: sequence + + Iteratively performs an action while refreshing the state of the visualization. + + Parameters: + + options - Some sequence options like + + - _condition_ A function returning a boolean instance in order to stop iterations. + - _step_ A function to execute on each step of the iteration. + - _onComplete_ A function to execute when the sequence finishes. + - _duration_ Duration (in milliseconds) of each step. + + Example: + (start code js) + var rg = new RGraph(canvas, config); //can be also Hypertree or ST + var i = 0; + rg.fx.sequence({ + condition: function() { + return i == 10; + }, + step: function() { + alert(i++); + }, + onComplete: function() { + alert('done!'); + } + }); + (end code) + + */ + sequence: function(options) { + var that = this; + options = $merge({ + condition: $lambda(false), + step: $empty, + onComplete: $empty, + duration: 200 + }, options || {}); + + var interval = setInterval(function() { + if(options.condition()) { + options.step(); + } else { + clearInterval(interval); + options.onComplete(); + } + that.viz.refresh(true); + }, options.duration); + }, + + /* + Method: animate + + Animates a by interpolating some properties. + + Parameters: + + opt - Animation options. This object contains as properties + + - _modes_ (required) An Array of animation types. Possible values are "linear", "polar", "moebius", "fade:nodes" and "fade:vertex". + + "linear", "polar" and "moebius" animation options will interpolate "startPos" and "endPos" properties, storing the result in "pos". + + "fade:nodes" and "fade:vertex" animation options will interpolate and/or "startAlpha" and "endAlpha" properties, storing the result in "alpha". + + - _duration_ Duration (in milliseconds) of the Animation. + - _fps_ Frames per second. + - _hideLabels_ hide labels or not during the animation. + + ...and other , or controller methods. + + Example: + (start code js) + var rg = new RGraph(canvas, config); //can be also Hypertree or ST + rg.fx.animate({ + modes: ['linear'], + hideLabels: false + }); + (end code) + + + */ + animate: function(opt, versor) { + var that = this, + viz = this.viz, + graph = viz.graph, + GUtil = Graph.Util; + opt = $merge(viz.controller, opt || {}); + + if(opt.hideLabels) this.hideLabels(true); + this.animation.setOptions($merge(opt, { + $animating: false, + compute: function(delta) { + var vector = versor? versor.scale(-delta) : null; + GUtil.eachNode(graph, function(node) { + for(var i=0; i. + + Parameters: + + opt - _optional_ Plotting options. + + Example: + + (start code js) + var rg = new RGraph(canvas, config); //can be also Hypertree or ST + rg.fx.plot(); + (end code) + + */ + plot: function(opt, animating) { + var viz = this.viz, + aGraph = viz.graph, + canvas = viz.canvas, + id = viz.root, + that = this, + ctx = canvas.getCtx(), + GUtil = Graph.Util; + opt = opt || this.viz.controller; + opt.clearCanvas && canvas.clear(); + + var T = !!aGraph.getNode(id).visited; + GUtil.eachNode(aGraph, function(node) { + GUtil.eachAdjacency(node, function(adj) { + var nodeTo = adj.nodeTo; + if(!!nodeTo.visited === T && node.drawn && nodeTo.drawn) { + !animating && opt.onBeforePlotLine(adj); + ctx.save(); + ctx.globalAlpha = Math.min(Math.min(node.alpha, nodeTo.alpha), adj.alpha); + that.plotLine(adj, canvas, animating); + ctx.restore(); + !animating && opt.onAfterPlotLine(adj); + } + }); + ctx.save(); + if(node.drawn) { + ctx.globalAlpha = node.alpha; + !animating && opt.onBeforePlotNode(node); + that.plotNode(node, canvas, animating); + !animating && opt.onAfterPlotNode(node); + } + if(!that.labelsHidden && opt.withLabels) { + if(node.drawn && ctx.globalAlpha >= 0.95) { + that.plotLabel(canvas, node, opt); + } else { + that.hideLabel(node, false); + } + } + ctx.restore(); + node.visited = !T; + }); + }, + + /* + Method: plotLabel + + Plots a label for a given node. + + Parameters: + + canvas - A instance. + node - A . + controller - A configuration object. See also , , . + + */ + plotLabel: function(canvas, node, controller) { + var id = node.id, tag = this.getLabel(id); + if(!tag && !(tag = document.getElementById(id))) { + tag = document.createElement('div'); + var container = this.getLabelContainer(); + container.appendChild(tag); + tag.id = id; + tag.className = 'node'; + tag.style.position = 'absolute'; + controller.onCreateLabel(tag, node); + this.labels[node.id] = tag; + } + this.placeLabel(tag, node, controller); + }, + + /* + Method: plotNode + + Plots a . + + Parameters: + + node - A . + canvas - A element. + + */ + plotNode: function(node, canvas, animating) { + var nconfig = this.node, data = node.data; + var cond = nconfig.overridable && data; + var width = cond && data.$lineWidth || nconfig.lineWidth; + var color = cond && data.$color || nconfig.color; + var ctx = canvas.getCtx(); + + ctx.lineWidth = width; + ctx.fillStyle = color; + ctx.strokeStyle = color; + + var f = node.data && node.data.$type || nconfig.type; + this.nodeTypes[f].call(this, node, canvas, animating); + }, + + /* + Method: plotLine + + Plots a line. + + Parameters: + + adj - A . + canvas - A instance. + + */ + plotLine: function(adj, canvas, animating) { + var econfig = this.edge, data = adj.data; + var cond = econfig.overridable && data; + var width = cond && data.$lineWidth || econfig.lineWidth; + var color = cond && data.$color || econfig.color; + var ctx = canvas.getCtx(); + + ctx.lineWidth = width; + ctx.fillStyle = color; + ctx.strokeStyle = color; + + var f = adj.data && adj.data.$type || econfig.type; + this.edgeTypes[f].call(this, adj, canvas, animating); + }, + + /* + Method: fitsInCanvas + + Returns _true_ or _false_ if the label for the node is contained in the canvas dom element or not. + + Parameters: + + pos - A instance (I'm doing duck typing here so any object with _x_ and _y_ parameters will do). + canvas - A instance. + + Returns: + + A boolean value specifying if the label is contained in the DOM element or not. + + */ + fitsInCanvas: function(pos, canvas) { + var size = canvas.getSize(); + if(pos.x >= size.width || pos.x < 0 + || pos.y >= size.height || pos.y < 0) return false; + return true; + } +}; + + +/* + * File: Loader.js + * + * Provides methods for loading JSON data. + * + * Description: + * + * Provides the method implemented by the main visualization classes to load JSON Trees and Graphs. + * + * Implemented By: + * + * , , and classes + * + */ + +/* + Object: Loader + + Provides static methods for loading JSON data. + + Description: + + This object is extended by the main Visualization classes (, , and ) + in order to implement the method. + + The method accepts JSON Trees and Graph objects. This will be explained in detail in the method definition. +*/ +var Loader = { + construct: function(json) { + var isGraph = ($type(json) == 'array'); + var ans = new Graph(this.graphOptions); + if(!isGraph) + //make tree + (function (ans, json) { + ans.addNode(json); + for(var i=0, ch = json.children; i and transition classes. + * +*/ + +/* + Object: Trans + + An object containing multiple type of transformations. + + Based on: + + Easing and Transition animation methods are based in the MooTools Framework . Copyright (c) 2006-2009 Valerio Proietti, . MIT license . + + Used by: + + , and classes. + + Description: + + This object is used for specifying different animation transitions in the , and classes. + + There are many different type of animation transitions. + + linear: + + Displays a linear transition + + >Trans.linear + + (see Linear.png) + + Quad: + + Displays a Quadratic transition. + + >Trans.Quad.easeIn + >Trans.Quad.easeOut + >Trans.Quad.easeInOut + + (see Quad.png) + + Cubic: + + Displays a Cubic transition. + + >Trans.Cubic.easeIn + >Trans.Cubic.easeOut + >Trans.Cubic.easeInOut + + (see Cubic.png) + + Quart: + + Displays a Quartetic transition. + + >Trans.Quart.easeIn + >Trans.Quart.easeOut + >Trans.Quart.easeInOut + + (see Quart.png) + + Quint: + + Displays a Quintic transition. + + >Trans.Quint.easeIn + >Trans.Quint.easeOut + >Trans.Quint.easeInOut + + (see Quint.png) + + Expo: + + Displays an Exponential transition. + + >Trans.Expo.easeIn + >Trans.Expo.easeOut + >Trans.Expo.easeInOut + + (see Expo.png) + + Circ: + + Displays a Circular transition. + + >Trans.Circ.easeIn + >Trans.Circ.easeOut + >Trans.Circ.easeInOut + + (see Circ.png) + + Sine: + + Displays a Sineousidal transition. + + >Trans.Sine.easeIn + >Trans.Sine.easeOut + >Trans.Sine.easeInOut + + (see Sine.png) + + Back: + + >Trans.Back.easeIn + >Trans.Back.easeOut + >Trans.Back.easeInOut + + (see Back.png) + + Bounce: + + Bouncy transition. + + >Trans.Bounce.easeIn + >Trans.Bounce.easeOut + >Trans.Bounce.easeInOut + + (see Bounce.png) + + Elastic: + + Elastic curve. + + >Trans.Elastic.easeIn + >Trans.Elastic.easeOut + >Trans.Elastic.easeInOut + + (see Elastic.png) + + + +*/ +this.Trans = { + linear: function(p) { return p; } +}; + +(function() { + + var makeTrans = function(transition, params){ + params = $splat(params); + return $extend(transition, { + easeIn: function(pos){ + return transition(pos, params); + }, + easeOut: function(pos){ + return 1 - transition(1 - pos, params); + }, + easeInOut: function(pos){ + return (pos <= 0.5) ? transition(2 * pos, params) / 2 : (2 - transition(2 * (1 - pos), params)) / 2; + } + }); + }; + + var transitions = { + + Pow: function(p, x){ + return Math.pow(p, x[0] || 6); + }, + + Expo: function(p){ + return Math.pow(2, 8 * (p - 1)); + }, + + Circ: function(p){ + return 1 - Math.sin(Math.acos(p)); + }, + + Sine: function(p){ + return 1 - Math.sin((1 - p) * Math.PI / 2); + }, + + Back: function(p, x){ + x = x[0] || 1.618; + return Math.pow(p, 2) * ((x + 1) * p - x); + }, + + Bounce: function(p){ + var value; + for (var a = 0, b = 1; 1; a += b, b /= 2){ + if (p >= (7 - 4 * a) / 11){ + value = b * b - Math.pow((11 - 6 * a - 11 * p) / 4, 2); + break; + } + } + return value; + }, + + Elastic: function(p, x){ + return Math.pow(2, 10 * --p) * Math.cos(20 * p * Math.PI * (x[0] || 1) / 3); + } + + }; + + $each(transitions, function(val, key) { + Trans[key] = makeTrans(val); + }); + + $each(['Quad', 'Cubic', 'Quart', 'Quint'], function(elem, i) { + Trans[elem] = makeTrans(function(p){ + return Math.pow(p, [i + 2]); + }); + }); + +})(); + +/* + Class: Animation + + A Class that can perform animations for generic objects. + + If you are looking for animation transitions please take a look at the object. + + Used by: + + + + Based on: + + The Animation class is based in the MooTools Framework . Copyright (c) 2006-2009 Valerio Proietti, . MIT license . + +*/ + +var Animation = new Class({ + + initalize: function(options) { + this.setOptions(options); + }, + + setOptions: function(options) { + var opt = { + duration: 2500, + fps: 40, + transition: Trans.Quart.easeInOut, + compute: $empty, + complete: $empty + }; + this.opt = $merge(opt, options || {}); + return this; + }, + + getTime: function() { + return $time(); + }, + + step: function(){ + var time = this.getTime(), opt = this.opt; + if (time < this.time + opt.duration){ + var delta = opt.transition((time - this.time) / opt.duration); + opt.compute(delta); + } else { + this.timer = clearInterval(this.timer); + opt.compute(1); + opt.complete(); + } + }, + + start: function(){ + this.time = 0; + this.startTimer(); + return this; + }, + + startTimer: function(){ + var that = this, opt = this.opt; + if (this.timer) return false; + this.time = this.getTime() - this.time; + this.timer = setInterval((function () { that.step(); }), Math.round(1000 / opt.fps)); + return true; + } +}); + + + +/* + * File: Spacetree.js + * + * Implements the class and other derived classes. + * + * Description: + * + * The main idea of the spacetree algorithm is to take the most common tree layout, and to expand nodes that are "context-related" .i.e lying on the path between the root node and a selected node. Useful animations to contract and expand nodes are also included. + * + * Inspired by: + * + * SpaceTree: Supporting Exploration in Large Node Link Tree, Design Evolution and Empirical Evaluation (Catherine Plaisant, Jesse Grosjean, Benjamin B. Bederson) + * + * + * + * Disclaimer: + * + * This visualization was built from scratch, taking only the paper as inspiration, and only shares some features with the Spacetree. + * + */ + +/* + Class: ST + + The main ST class + + Extends: + + + + Parameters: + + canvas - A Class + config - A configuration/controller object. + + Configuration: + + The configuration object can have the following properties (all properties are optional and have a default value) + + *General* + + - _levelsToShow_ Depth of the plotted tree. The plotted tree will be pruned in order to fit the specified depth if constrained=true. Default's 2. + - _constrained_ If true, the algorithm will try to plot only the part of the tree that fits the Canvas. + - _subtreeOffset_ Separation offset between subtrees. Default's 8. + - _siblingOffset_ Separation offset between siblings. Default's 5. + - _levelDistance_ Distance between levels. Default's 30. + - _orientation_ Sets the orientation layout. Implemented orientations are _left_ (the root node will be placed on the left side of the screen), _top_ (the root node will be placed on top of the screen), _bottom_ and _right_. Default's "left". + - _align_ Whether the tree alignment is left, center or right. + - _indent_ Used when alignment is left or right and shows an indentation between parent and children. Default's 10. + - _withLabels_ Whether the visualization should use/create labels or not. Default's *true*. + + *Node* + + Customize the visualization nodes' shape, color, and other style properties. + + - _Node_ + + This object has as properties + + - _overridable_ Determine whether or not nodes properties can be overriden by a particular node. Default's false. + + If given a JSON tree or graph, a node _data_ property contains properties which are the same as defined here but prefixed with + a dollar sign (i.e $), the node properties will override the global node properties. + + - _type_ Node type (shape). Possible options are "none", "square", "rectangle", "ellipse" and "circle". Default's "rectangle". + - _color_ Node color. Default's '#ccb'. + - _lineWidth_ Line width. If nodes aren't drawn with strokes then this property won't be of any use. Default's 1. + - _height_ Node height. Used for plotting rectangular nodes. _Width_ and _height_ properties are also used as bounding box for nodes of different shapes. + This means that for all shapes you'd have to make sure that the node's shape is contained in the bounding box defined by _width_ and _height_ parameters. Default's 20. + - _width_ Node width. Used for plotting rectangular nodes and for calculating a node's bounding box. Default's 90. + - _dim_ An extra parameter used by other complex shapes such as square and circle to determine the shape's diameter. + Please notice that even if these complex shapes (square, circle) only use _dim_ for calculating it's diameter property, the complex shape must be + contained in the bounding box calculated with the _width_ and _height_ parameters. Default's 15. + - _align_ Defines a node's alignment. Possible values are "center", "left", "right". Default's "center". + + *Edge* + + Customize the visualization edges' shape, color, and other style properties. + + - _Edge_ + + This object has as properties + + - _overridable_ Determine whether or not edges properties can be overriden by a particular edge object. Default's false. + + If given a JSON _complex_ graph (defined in ), an adjacency _data_ property contains properties which are the same as defined here but prefixed with + a dollar sign (i.e $), the adjacency properties will override the global edge properties. + + - _type_ Edge type (shape). Possible options are "none", "line", "quadratic:begin", "quadratic:end", "bezier" and "arrow". Default's "line". + - _color_ Edge color. Default's '#ccb'. + - _lineWidth_ Line width. If edges aren't drawn with strokes then this property won't be of any use. Default's 1. + - _dim_ An extra parameter used by other complex shapes such as quadratic, arrow and bezier to determine the shape's diameter. + + *Animations* + + - _duration_ Duration of the animation in milliseconds. Default's 700. + - _fps_ Frames per second. Default's 25. + - _transition_ One of the transitions defined in the class. Default's Quart.easeInOut. + - _clearCanvas_ Whether to clear canvas on each animation frame or not. Default's true. + + *Controller options* + + You can also implement controller functions inside the configuration object. This functions are + + - _onBeforeCompute(node)_ This method is called right before performing all computation and animations to the JIT visualization. + - _onAfterCompute()_ This method is triggered right after all animations or computations for the JIT visualizations ended. + - _onCreateLabel(domElement, node)_ This method receives the label dom element as first parameter, and the corresponding as second parameter. This method will only be called on label creation. Note that a is a node from the input tree/graph you provided to the visualization. If you want to know more about what kind of JSON tree/graph format is used to feed the visualizations, you can take a look at . This method proves useful when adding events to the labels used by the JIT. + - _onPlaceLabel(domElement, node)_ This method receives the label dom element as first parameter and the corresponding as second parameter. This method is called each time a label has been placed on the visualization, and thus it allows you to update the labels properties, such as size or position. Note that onPlaceLabel will be triggered after updating the labels positions. That means that, for example, the left and top css properties are already updated to match the nodes positions. + - _onBeforePlotNode(node)_ This method is triggered right before plotting a given node. The _node_ parameter is the to be plotted. +This method is useful for changing a node style right before plotting it. + - _onAfterPlotNode(node)_ This method is triggered right after plotting a given node. The _node_ parameter is the plotted. + - _onBeforePlotLine(adj)_ This method is triggered right before plotting an edge. The _adj_ parameter is a object. +This method is useful for adding some styles to a particular edge before being plotted. + - _onAfterPlotLine(adj)_ This method is triggered right after plotting an edge. The _adj_ parameter is the plotted. + - _request(nodeId, level, onComplete)_ This method is used for buffering information into the visualization. When clicking on an empty node, + the visualization will make a request for this node's subtree, specifying a given level for this subtree. Once this request is completed the _onComplete_ +object must be called with the given result. + + Example: + + Here goes a complete example. In most cases you won't be forced to implement all properties and methods. In fact, + all configuration properties have the default value assigned. + + I won't be instanciating a class here. If you want to know more about instanciating a class + please take a look at the class documentation. + + (start code js) + var st = new ST(canvas, { + orientation: "left", + levelsToShow: 2, + subtreeOffset: 8, + siblingOffset: 5, + levelDistance: 30, + withLabels: true, + align: "center", + multitree: false, + indent: 10, + Node: { + overridable: false, + type: 'rectangle', + color: '#ccb', + lineWidth: 1, + height: 20, + width: 90, + dim: 15, + align: "center" + }, + Edge: { + overridable: false, + type: 'line', + color: '#ccc', + dim: 15, + lineWidth: 1 + }, + duration: 700, + fps: 25, + transition: Trans.Quart.easeInOut, + clearCanvas: true, + + onBeforeCompute: function(node) { + //do something onBeforeCompute + }, + onAfterCompute: function () { + //do something onAfterCompute + }, + onCreateLabel: function(domElement, node) { + //do something onCreateLabel + }, + onPlaceLabel: function(domElement, node) { + //do something onPlaceLabel + }, + onBeforePlotNode:function(node) { + //do something onBeforePlotNode + }, + onAfterPlotNode: function(node) { + //do something onAfterPlotNode + }, + onBeforePlotLine:function(adj) { + //do something onBeforePlotLine + }, + onAfterPlotLine: function(adj) { + //do something onAfterPlotLine + }, + request: false + + }); + (end code) + + Instance Properties: + + - _graph_ Access a instance. + - _op_ Access a instance. + - _fx_ Access a instance. + */ + +(function () { + +//Layout functions +var slice = Array.prototype.slice; + +/* + Calculates the max width and height nodes for a tree level +*/ +function getBoundaries(graph, config, level, orn) { + var dim = config.Node, GUtil = Graph.Util; + var multitree = config.multitree; + if(dim.overridable) { + var w = -1, h = -1; + GUtil.eachNode(graph, function(n) { + if(n._depth == level && (!multitree || + ('$orn' in n.data) && + n.data.$orn == orn)) { + var dw = n.data.$width || dim.width; + var dh = n.data.$height || dim.height; + w = (w < dw)? dw : w; + h = (h < dh)? dh : h; + } + }); + return { + 'width': w < 0? dim.width : w, + 'height': h < 0? dim.height : h + }; + } else { + return dim; + } +}; + + function movetree(node, prop, val, orn) { + var p = (orn == "left" || orn == "right")? "y" : "x"; + node[prop][p] += val; + }; + + function moveextent(extent, val) { + var ans = []; + $each(extent, function(elem) { + elem = slice.call(elem); + elem[0] += val; + elem[1] += val; + ans.push(elem); + }); + return ans; + }; + + function merge(ps, qs) { + if(ps.length == 0) return qs; + if(qs.length == 0) return ps; + var p = ps.shift(), q = qs.shift(); + return [[p[0], q[1]]].concat(merge(ps, qs)); + }; + + function mergelist(ls, def) { + def = def || []; + if(ls.length == 0) return def; + var ps = ls.pop(); + return mergelist(ls, merge(ps, def)); + }; + + function fit(ext1, ext2, subtreeOffset, siblingOffset, i) { + if(ext1.length <= i || + ext2.length <= i) return 0; + + var p = ext1[i][1], q = ext2[i][0]; + return Math.max(fit(ext1, ext2, subtreeOffset, siblingOffset, ++i) + + subtreeOffset, p - q + siblingOffset); + }; + + function fitlistl(es, subtreeOffset, siblingOffset) { + function $fitlistl(acc, es, i) { + if(es.length <= i) return []; + var e = es[i], ans = fit(acc, e, subtreeOffset, siblingOffset, 0); + return [ans].concat($fitlistl(merge(acc, moveextent(e, ans)), es, ++i)); + }; + return $fitlistl([], es, 0); + }; + + function fitlistr(es, subtreeOffset, siblingOffset) { + function $fitlistr(acc, es, i) { + if(es.length <= i) return []; + var e = es[i], ans = -fit(e, acc, subtreeOffset, siblingOffset, 0); + return [ans].concat($fitlistr(merge(moveextent(e, ans), acc), es, ++i)); + }; + es = slice.call(es); + var ans = $fitlistr([], es.reverse(), 0); + return ans.reverse(); + }; + + function fitlist(es, subtreeOffset, siblingOffset, align) { + var esl = fitlistl(es, subtreeOffset, siblingOffset), + esr = fitlistr(es, subtreeOffset, siblingOffset); + + if(align == "left") + esr = esl; + else if(align == "right") + esl = esr; + + for(var i = 0, ans = []; i < esl.length; i++) { + ans[i] = (esl[i] + esr[i]) / 2; + } + return ans; + }; + + function design(graph, node, prop, config, orn) { + var multitree = config.multitree; + var auxp = ['x', 'y'], auxs = ['width', 'height']; + var ind = +(orn == "left" || orn == "right"); + var p = auxp[ind], notp = auxp[1 - ind]; + + var cnode = config.Node; + var s = auxs[ind], nots = auxs[1 - ind]; + + var siblingOffset = config.siblingOffset; + var subtreeOffset = config.subtreeOffset; + var align = config.align; + + var GUtil = Graph.Util; + + function $design(node, maxsize, acum) { + var sval = (cnode.overridable && node.data["$" + s]) || cnode[s]; + var notsval = maxsize || ((cnode.overridable && node.data["$" + nots]) || cnode[nots]); + + var trees = [], extents = [], chmaxsize = false; + var chacum = notsval + config.levelDistance; + GUtil.eachSubnode(node, function(n) { + if(n.exist && (!multitree || + ('$orn' in n.data) && + n.data.$orn == orn)) { + + if(!chmaxsize) + chmaxsize = getBoundaries(graph, config, n._depth, orn); + + var s = $design(n, chmaxsize[nots], acum + chacum); + trees.push(s.tree); + extents.push(s.extent); + } + }); + var positions = fitlist(extents, subtreeOffset, siblingOffset, align); + for(var i=0, ptrees = [], pextents = []; i < trees.length; i++) { + movetree(trees[i], prop, positions[i], orn); + pextents.push(moveextent(extents[i], positions[i])); + } + var resultextent = [[-sval/2, sval/2]].concat(mergelist(pextents)); + node[prop][p] = 0; + + if (orn == "top" || orn == "left") { + node[prop][notp] = acum; + } else { + node[prop][notp] = -acum; + } + + return { + tree: node, + extent: resultextent + }; + }; + $design(node, false, 0); + }; + + + +this.ST= (function() { + //Define some private methods first... + //Nodes in path + var nodesInPath = []; + //Nodes to contract + function getNodesToHide(node) { + node = node || this.clickedNode; + var Geom = this.geom, GUtil = Graph.Util; + var graph = this.graph; + var canvas = this.canvas; + var level = node._depth, nodeArray = []; + GUtil.eachNode(graph, function(n) { + if(n.exist && !n.selected) { + if(GUtil.isDescendantOf(n, node.id)) { + if(n._depth <= level) nodeArray.push(n); + } else { + nodeArray.push(n); + } + } + }); + var leafLevel = Geom.getRightLevelToShow(node, canvas); + GUtil.eachLevel(node, leafLevel, leafLevel, function(n) { + if(n.exist && !n.selected) nodeArray.push(n); + }); + + for (var i = 0; i < nodesInPath.length; i++) { + var n = this.graph.getNode(nodesInPath[i]); + if(!GUtil.isDescendantOf(n, node.id)) { + nodeArray.push(n); + } + } + return nodeArray; + }; + //Nodes to expand + function getNodesToShow(node) { + var nodeArray= [], GUtil = Graph.Util, config = this.config; + node = node || this.clickedNode; + GUtil.eachLevel(this.clickedNode, 0, config.levelsToShow, function(n) { + if(config.multitree && !('$orn' in n.data) + && GUtil.anySubnode(n, function(ch){ return ch.exist && !ch.drawn; })) { + nodeArray.push(n); + } else if(n.drawn && !GUtil.anySubnode(n, "drawn")) { + nodeArray.push(n); + } + }); + return nodeArray; + }; + //Now define the actual class. + return new Class({ + + Implements: Loader, + + initialize: function(canvas, controller) { + var innerController = { + onBeforeCompute: $empty, + onAfterCompute: $empty, + onCreateLabel: $empty, + onPlaceLabel: $empty, + onComplete: $empty, + onBeforePlotNode:$empty, + onAfterPlotNode: $empty, + onBeforePlotLine:$empty, + onAfterPlotLine: $empty, + request: false + }; + + var config= { + orientation: "left", + labelContainer: canvas.id + '-label', + levelsToShow: 2, + subtreeOffset: 8, + siblingOffset: 5, + levelDistance: 30, + withLabels: true, + clearCanvas: true, + align: "center", + indent:10, + multitree: false, + constrained: true, + + Node: { + overridable: false, + type: 'rectangle', + color: '#ccb', + lineWidth: 1, + height: 20, + width: 90, + dim: 15, + align: "center" + }, + Edge: { + overridable: false, + type: 'line', + color: '#ccc', + dim: 15, + lineWidth: 1 + }, + duration: 700, + fps: 25, + transition: Trans.Quart.easeInOut + }; + + this.controller = this.config = $merge(config, innerController, controller); + this.canvas = canvas; + this.graphOptions = { + 'complex': true + }; + this.graph = new Graph(this.graphOptions); + this.fx = new ST.Plot(this); + this.op = new ST.Op(this); + this.group = new ST.Group(this); + this.geom = new ST.Geom(this); + this.clickedNode= null; + + }, + + /* + Method: plot + + Plots the tree. Usually this method is called right after computing nodes' positions. + + */ + plot: function() { this.fx.plot(this.controller); }, + + + /* + Method: switchPosition + + Switches the tree orientation. + + Parameters: + + pos - The new tree orientation. Possible values are "top", "left", "right" and "bottom". + method - Set this to "animate" if you want to animate the tree when switching its position. You can also set this parameter to "replot" to just replot the subtree. + onComplete - _optional_ This callback is called once the "switching" animation is complete. + + Example: + + (start code js) + st.switchPosition("right", "animate", { + onComplete: function() { + alert('completed!'); + } + }); + (end code) + */ + switchPosition: function(pos, method, onComplete) { + var Geom = this.geom, Plot = this.fx, that = this; + if(!Plot.busy) { + Plot.busy = true; + this.contract({ + onComplete: function() { + Geom.switchOrientation(pos); + that.compute('endPos', false); + Plot.busy = false; + if(method == 'animate') { + that.onClick(that.clickedNode.id, onComplete); + } else if(method == 'replot') { + that.select(that.clickedNode.id, onComplete); + } + } + }, pos); + } + }, + + /* + Method: switchAlignment + + Switches the tree alignment. + + Parameters: + + align - The new tree alignment. Possible values are "left", "center" and "right". + method - Set this to "animate" if you want to animate the tree after aligning its position. You can also set this parameter to "replot" to just replot the subtree. + onComplete - _optional_ This callback is called once the "switching" animation is complete. + + Example: + + (start code js) + st.switchAlignment("right", "animate", { + onComplete: function() { + alert('completed!'); + } + }); + (end code) + */ + switchAlignment: function(align, method, onComplete) { + this.config.align = align; + if(method == 'animate') { + this.select(this.clickedNode.id, onComplete); + } else if(method == 'replot') { + this.onClick(this.clickedNode.id, onComplete); + } + }, + + /* + Method: addNodeInPath + + Adds a node to the current path as selected node. This node will be visible (as in non-collapsed) at all times. + + + Parameters: + + id - A id. + + Example: + + (start code js) + st.addNodeInPath("somenodeid"); + (end code) + */ + addNodeInPath: function(id) { + nodesInPath.push(id); + this.select((this.clickedNode && this.clickedNode.id) || this.root); + }, + + /* + Method: clearNodesInPath + + Removes all nodes tagged as selected by the method. + + See also: + + + + Example: + + (start code js) + st.clearNodesInPath(); + (end code) + */ + clearNodesInPath: function(id) { + nodesInPath.length = 0; + this.select((this.clickedNode && this.clickedNode.id) || this.root); + }, + + /* + Method: refresh + + Computes nodes' positions and replots the tree. + + */ + refresh: function() { + this.reposition(); + this.select((this.clickedNode && this.clickedNode.id) || this.root); + }, + + reposition: function() { + Graph.Util.computeLevels(this.graph, this.root, 0, "ignore"); + this.geom.setRightLevelToShow(this.clickedNode, this.canvas); + Graph.Util.eachNode(this.graph, function(n) { + if(n.exist) n.drawn = true; + }); + this.compute('endPos'); + }, + + /* + Method: compute + + Computes nodes' positions. + + */ + compute: function (property, computeLevels) { + var prop = property || 'startPos'; + var node = this.graph.getNode(this.root); + $extend(node, { + 'drawn':true, + 'exist':true, + 'selected':true + }); + if(!!computeLevels || !("_depth" in node)) + Graph.Util.computeLevels(this.graph, this.root, 0, "ignore"); + this.computePositions(node, prop); + }, + + computePositions: function(node, prop) { + var config = this.config; + var multitree = config.multitree; + var align = config.align; + var indent = align !== 'center' && config.indent; + var orn = config.orientation; + var orns = multitree? ['top', 'right', 'bottom', 'left'] : [orn]; + + var that = this; + $each(orns, function(orn) { + //calculate layout + design(that.graph, node, prop, that.config, orn); + var i = ['x', 'y'][+(orn == "left" || orn == "right")]; + //absolutize + (function red(node) { + Graph.Util.eachSubnode(node, function(n) { + if(n.exist && (!multitree || + ('$orn' in n.data) && + n.data.$orn == orn)) { + + n[prop][i] += node[prop][i]; + if(indent) { + n[prop][i] += align == 'left'? indent : -indent; + } + red(n); + } + }); + })(node); + }); + }, + + requestNodes: function(node, onComplete) { + var handler = $merge(this.controller, onComplete), + lev = this.config.levelsToShow, GUtil = Graph.Util; + if(handler.request) { + var leaves = [], d = node._depth; + GUtil.eachLevel(node, 0, lev, function(n) { + if(n.drawn && + !GUtil.anySubnode(n)) { + leaves.push(n); + n._level = lev - (n._depth - d); + } + }); + this.group.requestNodes(leaves, handler); + } + else + handler.onComplete(); + }, + + contract: function(onComplete, switched) { + var orn = this.config.orientation; + var Geom = this.geom, Group = this.group; + if(switched) Geom.switchOrientation(switched); + var nodes = getNodesToHide.call(this); + if(switched) Geom.switchOrientation(orn); + Group.contract(nodes, $merge(this.controller, onComplete)); + }, + + move: function(node, onComplete) { + this.compute('endPos', false); + var move = onComplete.Move, offset = { + 'x': move.offsetX, + 'y': move.offsetY + }; + if(move.enable) { + this.geom.translate(node.endPos.add(offset).$scale(-1), "endPos"); + } + this.fx.animate($merge(this.controller, { modes: ['linear'] }, onComplete)); + }, + + + expand: function (node, onComplete) { + var nodeArray = getNodesToShow.call(this, node); + this.group.expand(nodeArray, $merge(this.controller, onComplete)); + }, + + + selectPath: function(node) { + var GUtil = Graph.Util, that = this; + GUtil.eachNode(this.graph, function(n) { n.selected = false; }); + function path(node) { + if(node == null || node.selected) return; + node.selected = true; + $each(that.group.getSiblings([node])[node.id], + function(n) { + n.exist = true; + n.drawn = true; + }); + var parents = GUtil.getParents(node); + parents = (parents.length > 0)? parents[0] : null; + path(parents); + }; + for(var i=0, ns = [node.id].concat(nodesInPath); i < ns.length; i++) { + path(this.graph.getNode(ns[i])); + } + }, + + /* + Method: setRoot + + Switches the current root node. + + Parameters: + id - The id of the node to be set as root. + method - Set this to "animate" if you want to animate the tree after adding the subtree. You can also set this parameter to "replot" to just replot the subtree. + onComplete - _optional_ An action to perform after the animation (if any). + + Example: + + (start code js) + st.setRoot('my_node_id', 'animate', { + onComplete: function() { + alert('complete!'); + } + }); + (end code) + */ + setRoot: function(id, method, onComplete) { + var that = this, canvas = this.canvas; + var rootNode = this.graph.getNode(this.root); + var clickedNode = this.graph.getNode(id); + function $setRoot() { + if(this.config.multitree && clickedNode.data.$orn) { + var orn = clickedNode.data.$orn; + var opp = { + 'left': 'right', + 'right': 'left', + 'top': 'bottom', + 'bottom': 'top' + }[orn]; + rootNode.data.$orn = opp; + (function tag(rootNode) { + Graph.Util.eachSubnode(rootNode, function(n) { + if(n.id != id) { + n.data.$orn = opp; + tag(n); + } + }); + })(rootNode); + delete clickedNode.data.$orn; + } + this.root = id; + this.clickedNode = clickedNode; + Graph.Util.computeLevels(this.graph, this.root, 0, "ignore"); + } + + //delete previous orientations (if any) + delete rootNode.data.$orns; + + if(method == 'animate') { + this.onClick(id, { + onBeforeMove: function() { + $setRoot.call(that); + that.selectPath(clickedNode); + } + }); + } else if(method == 'replot') { + $setRoot.call(this); + this.select(this.root); + } + }, + + /* + Method: addSubtree + + Adds a subtree, performing optionally an animation. + + Parameters: + subtree - A JSON Tree object. See also . + method - Set this to "animate" if you want to animate the tree after adding the subtree. You can also set this parameter to "replot" to just replot the subtree. + onComplete - _optional_ An action to perform after the animation (if any). + + Example: + + (start code js) + st.addSubtree(json, 'animate', { + onComplete: function() { + alert('complete!'); + } + }); + (end code) + */ + addSubtree: function(subtree, method, onComplete) { + if(method == 'replot') { + this.op.sum(subtree, $extend({ type: 'replot' }, onComplete || {})); + } else if (method == 'animate') { + this.op.sum(subtree, $extend({ type: 'fade:seq' }, onComplete || {})); + } + }, + + /* + Method: removeSubtree + + Removes a subtree, performing optionally an animation. + + Parameters: + id - The _id_ of the subtree to be removed. + removeRoot - Remove the root of the subtree or only its subnodes. + method - Set this to "animate" if you want to animate the tree after removing the subtree. You can also set this parameter to "replot" to just replot the subtree. + onComplete - _optional_ An action to perform after the animation (if any). + + Example: + + (start code js) + st.removeSubtree('idOfSubtreeToBeRemoved', false, 'animate', { + onComplete: function() { + alert('complete!'); + } + }); + (end code) + + */ + removeSubtree: function(id, removeRoot, method, onComplete) { + var node = this.graph.getNode(id), subids = []; + Graph.Util.eachLevel(node, +!removeRoot, false, function(n) { + subids.push(n.id); + }); + if(method == 'replot') { + this.op.removeNode(subids, $extend({ type: 'replot' }, onComplete || {})); + } else if (method == 'animate') { + this.op.removeNode(subids, $extend({ type: 'fade:seq'}, onComplete || {})); + } + }, + + /* + Method: select + + Selects a sepecific node in the Spacetree without performing an animation. Useful when selecting + nodes which are currently hidden or deep inside the tree. + + Parameters: + id - The id of the node to select. + onComplete - _optional_ onComplete callback. + + Example: + (start code js) + st.select('mynodeid', { + onComplete: function() { + alert('complete!'); + } + }); + (end code) + */ + select: function(id, onComplete) { + var group = this.group, geom = this.geom; + var node= this.graph.getNode(id), canvas = this.canvas; + var root = this.graph.getNode(this.root); + var complete = $merge(this.controller, onComplete); + var that = this; + + complete.onBeforeCompute(node); + this.selectPath(node); + this.clickedNode= node; + this.requestNodes(node, { + onComplete: function(){ + group.hide(group.prepare(getNodesToHide.call(that)), complete); + geom.setRightLevelToShow(node, canvas); + that.compute("pos"); + Graph.Util.eachNode(that.graph, function(n) { + var pos = n.pos.getc(true); + n.startPos.setc(pos.x, pos.y); + n.endPos.setc(pos.x, pos.y); + n.visited = false; + }); + that.geom.translate(node.endPos.scale(-1), ["pos", "startPos", "endPos"]); + group.show(getNodesToShow.call(that)); + that.plot(); + complete.onAfterCompute(that.clickedNode); + complete.onComplete(); + } + }); + }, + + /* + Method: onClick + + This method is called when clicking on a tree node. It mainly performs all calculations and the animation of contracting, translating and expanding pertinent nodes. + + + Parameters: + + id - A node id. + options - A group of options and callbacks such as + + - _onComplete_ an object callback called when the animation finishes. + - _Move_ an object that has as properties _offsetX_ or _offsetY_ for adding some offset position to the centered node. + + Example: + + (start code js) + st.onClick('mynodeid', { + Move: { + enable: true, + offsetX: 30, + offsetY: 5 + }, + onComplete: function() { + alert('yay!'); + } + }); + (end code) + + */ + onClick: function (id, options) { + var canvas = this.canvas, that = this, Fx = this.fx, Util = Graph.Util, Geom = this.geom; + var innerController = { + Move: { + enable: true, + offsetX: 0, + offsetY: 0 + }, + onBeforeRequest: $empty, + onBeforeContract: $empty, + onBeforeMove: $empty, + onBeforeExpand: $empty + }; + var complete = $merge(this.controller, innerController, options); + + if(!this.busy) { + this.busy= true; + var node= this.graph.getNode(id); + this.selectPath(node, this.clickedNode); + this.clickedNode= node; + complete.onBeforeCompute(node); + complete.onBeforeRequest(node); + this.requestNodes(node, { + onComplete: function() { + complete.onBeforeContract(node); + that.contract({ + onComplete: function() { + Geom.setRightLevelToShow(node, canvas); + complete.onBeforeMove(node); + that.move(node, { + Move: complete.Move, + onComplete: function() { + complete.onBeforeExpand(node); + that.expand(node, { + onComplete: function() { + that.busy = false; + complete.onAfterCompute(id); + complete.onComplete(); + } + }); //expand + } + }); //move + } + });//contract + } + });//request + } + } + }); + +})(); + +/* + Class: ST.Op + + Performs advanced operations on trees and graphs. + + Extends: + + All methods + + Access: + + This instance can be accessed with the _op_ parameter of the st instance created. + + Example: + + (start code js) + var st = new ST(canvas, config); + st.op.morph //or can also call any other method + (end code) + +*/ +ST.Op = new Class({ + Implements: Graph.Op, + + initialize: function(viz) { + this.viz = viz; + } +}); + +/* + + Performs operations on group of nodes. + +*/ +ST.Group = new Class({ + + initialize: function(viz) { + this.viz = viz; + this.canvas = viz.canvas; + this.config = viz.config; + this.animation = new Animation; + this.nodes = null; + }, + + /* + + Calls the request method on the controller to request a subtree for each node. + */ + requestNodes: function(nodes, controller) { + var counter = 0, len = nodes.length, nodeSelected = {}; + var complete = function() { controller.onComplete(); }; + var viz = this.viz; + if(len == 0) complete(); + for(var i=0; i= b._depth); }); + for(var i=0; i 0 + && n.drawn) { + n.drawn = false; + nds[node.id].push(n); + } else if((!root || !orns) && n.drawn) { + n.drawn = false; + nds[node.id].push(n); + } + }); + node.drawn = true; + } + //plot the whole (non-scaled) tree + if(nodes.length > 0) viz.fx.plot(); + //show nodes that were previously hidden + for(i in nds) { + $each(nds[i], function(n) { n.drawn = true; }); + } + //plot each scaled subtree + for(i=0; i method + (end code) + +*/ + +ST.Geom = new Class({ + + initialize: function(viz) { + this.viz = viz; + this.config = viz.config; + this.node = viz.config.Node; + this.edge = viz.config.Edge; + }, + + /* + Method: translate + + Applies a translation to the tree. + + Parameters: + + pos - A number specifying translation vector. + prop - A position property ('pos', 'startPos' or 'endPos'). + + Example: + + (start code js) + st.geom.translate(new Complex(300, 100), 'endPos'); + (end code) + */ + translate: function(pos, prop) { + prop = $splat(prop); + Graph.Util.eachNode(this.viz.graph, function(elem) { + $each(prop, function(p) { elem[p].$add(pos); }); + }); + }, + + /* + Changes the tree current orientation to the one specified. + + You should usually use instead. + */ + switchOrientation: function(orn) { + this.config.orientation = orn; + }, + + /* + Makes a value dispatch according to the current layout + Works like a CSS property, either _top-right-bottom-left_ or _top|bottom - left|right_. + */ + dispatch: function() { + //TODO(nico) should store Array.prototype.slice.call somewhere. + var args = Array.prototype.slice.call(arguments); + var s = args.shift(), len = args.length; + var val = function(a) { return typeof a == 'function'? a() : a; }; + if(len == 2) { + return (s == "top" || s == "bottom")? val(args[0]) : val(args[1]); + } else if(len == 4) { + switch(s) { + case "top": return val(args[0]); + case "right": return val(args[1]); + case "bottom": return val(args[2]); + case "left": return val(args[3]); + } + } + return undefined; + }, + + /* + Returns label height or with, depending on the tree current orientation. + */ + getSize: function(n, invert) { + var node = this.node, data = n.data, config = this.config; + var cond = node.overridable, siblingOffset = config.siblingOffset; + var s = (this.config.multitree + && ('$orn' in n.data) + && n.data.$orn) || this.config.orientation; + var w = (cond && data.$width || node.width) + siblingOffset; + var h = (cond && data.$height || node.height) + siblingOffset; + if(!invert) + return this.dispatch(s, h, w); + else + return this.dispatch(s, w, h); + }, + + /* + Calculates a subtree base size. This is an utility function used by _getBaseSize_ + */ + getTreeBaseSize: function(node, level, leaf) { + var size = this.getSize(node, true), baseHeight = 0, that = this; + if(leaf(level, node)) return size; + if(level === 0) return 0; + Graph.Util.eachSubnode(node, function(elem) { + baseHeight += that.getTreeBaseSize(elem, level -1, leaf); + }); + return (size > baseHeight? size : baseHeight) + this.config.subtreeOffset; + }, + + + /* + Method: getEdge + + Returns a Complex instance with the begin or end position of the edge to be plotted. + + Parameters: + + node - A that is connected to this edge. + type - Returns the begin or end edge position. Possible values are 'begin' or 'end'. + + Returns: + + A number specifying the begin or end position. + */ + getEdge: function(node, type, s) { + var $C = function(a, b) { + return function(){ + return node.pos.add(new Complex(a, b)); + }; + }; + var dim = this.node; + var cond = this.node.overridable, data = node.data; + var w = cond && data.$width || dim.width; + var h = cond && data.$height || dim.height; + + if(type == 'begin') { + if(dim.align == "center") { + return this.dispatch(s, $C(0, h/2), $C(-w/2, 0), + $C(0, -h/2),$C(w/2, 0)); + } else if(dim.align == "left") { + return this.dispatch(s, $C(0, h), $C(0, 0), + $C(0, 0), $C(w, 0)); + } else if(dim.align == "right") { + return this.dispatch(s, $C(0, 0), $C(-w, 0), + $C(0, -h),$C(0, 0)); + } else throw "align: not implemented"; + + + } else if(type == 'end') { + if(dim.align == "center") { + return this.dispatch(s, $C(0, -h/2), $C(w/2, 0), + $C(0, h/2), $C(-w/2, 0)); + } else if(dim.align == "left") { + return this.dispatch(s, $C(0, 0), $C(w, 0), + $C(0, h), $C(0, 0)); + } else if(dim.align == "right") { + return this.dispatch(s, $C(0, -h),$C(0, 0), + $C(0, 0), $C(-w, 0)); + } else throw "align: not implemented"; + } + }, + + /* + Adjusts the tree position due to canvas scaling or translation. + */ + getScaledTreePosition: function(node, scale) { + var dim = this.node; + var cond = this.node.overridable, data = node.data; + var w = (cond && data.$width || dim.width); + var h = (cond && data.$height || dim.height); + var s = (this.config.multitree + && ('$orn' in node.data) + && node.data.$orn) || this.config.orientation; + + var $C = function(a, b) { + return function(){ + return node.pos.add(new Complex(a, b)).$scale(1 - scale); + }; + }; + if(dim.align == "left") { + return this.dispatch(s, $C(0, h), $C(0, 0), + $C(0, 0), $C(w, 0)); + } else if(dim.align == "center") { + return this.dispatch(s, $C(0, h / 2), $C(-w / 2, 0), + $C(0, -h / 2),$C(w / 2, 0)); + } else if(dim.align == "right") { + return this.dispatch(s, $C(0, 0), $C(-w, 0), + $C(0, -h),$C(0, 0)); + } else throw "align: not implemented"; + }, + + /* + Method: treeFitsInCanvas + + Returns a Boolean if the current subtree fits in canvas. + + Parameters: + + node - A which is the current root of the subtree. + canvas - The object. + level - The depth of the subtree to be considered. + */ + treeFitsInCanvas: function(node, canvas, level) { + var csize = canvas.getSize(node); + var s = (this.config.multitree + && ('$orn' in node.data) + && node.data.$orn) || this.config.orientation; + + var size = this.dispatch(s, csize.width, csize.height); + var baseSize = this.getTreeBaseSize(node, level, function(level, node) { + return level === 0 || !Graph.Util.anySubnode(node); + }); + return (baseSize < size); + }, + + /* + Hides levels of the tree until it properly fits in canvas. + */ + setRightLevelToShow: function(node, canvas) { + var level = this.getRightLevelToShow(node, canvas), fx = this.viz.fx; + Graph.Util.eachLevel(node, 0, this.config.levelsToShow, function(n) { + var d = n._depth - node._depth; + if(d > level) { + n.drawn = false; + n.exist = false; + fx.hideLabel(n, false); + } else { + n.exist = true; + } + }); + node.drawn= true; + }, + + /* + Returns the right level to show for the current tree in order to fit in canvas. + */ + getRightLevelToShow: function(node, canvas) { + var config = this.config; + var level = config.levelsToShow; + var constrained = config.constrained; + if(!constrained) return level; + while(!this.treeFitsInCanvas(node, canvas, level) && level > 1) { level-- ; } + return level; + } + +}); + +/* + Object: ST.Plot + + Performs plotting operations. + + Extends: + + All methods + + Access: + + This instance can be accessed with the _fx_ parameter of the st instance created. + + Example: + + (start code js) + var st = new ST(canvas, config); + st.fx.placeLabel //or can also call any other method + (end code) + + +*/ +ST.Plot = new Class({ + + Implements: Graph.Plot, + + initialize: function(viz) { + this.viz = viz; + this.config = viz.config; + this.node = this.config.Node; + this.edge = this.config.Edge; + this.animation = new Animation; + this.nodeTypes = new ST.Plot.NodeTypes; + this.edgeTypes = new ST.Plot.EdgeTypes; + }, + + /* + Plots a subtree from the spacetree. + */ + plotSubtree: function(node, opt, scale, animating) { + var viz = this.viz, canvas = viz.canvas; + scale = Math.min(Math.max(0.001, scale), 1); + if(scale >= 0) { + node.drawn = false; + var ctx = canvas.getCtx(); + var diff = viz.geom.getScaledTreePosition(node, scale); + ctx.translate(diff.x, diff.y); + ctx.scale(scale, scale); + } + this.plotTree(node, !scale, opt, animating); + if(scale >= 0) node.drawn = true; + }, + /* + Plots a Subtree. + */ + plotTree: function(node, plotLabel, opt, animating) { + var that = this, + viz = this.viz, + canvas = viz.canvas, + config = this.config, + ctx = canvas.getCtx(); + var root = config.multitree && !('$orn' in node.data); + var orns = root && node.data.$orns; + Graph.Util.eachSubnode(node, function(elem) { + //multitree root node check + if((!root || orns.indexOf(elem.data.$orn) > 0) + && elem.exist && elem.drawn) { + var adj = node.getAdjacency(elem.id); + !animating && opt.onBeforePlotLine(adj); + ctx.globalAlpha = Math.min(node.alpha, elem.alpha); + that.plotLine(adj, canvas, animating); + !animating && opt.onAfterPlotLine(adj); + that.plotTree(elem, plotLabel, opt, animating); + } + }); + + if(node.drawn) { + ctx.globalAlpha = node.alpha; + !animating && opt.onBeforePlotNode(node); + this.plotNode(node, canvas, animating); + !animating && opt.onAfterPlotNode(node); + if(plotLabel && ctx.globalAlpha >= 0.95) + this.plotLabel(canvas, node, opt); + else + this.hideLabel(node, false); + } else { + this.hideLabel(node, true); + } + }, + + /* + Method: placeLabel + + Overrides abstract method placeLabel in . + + Parameters: + + tag - A DOM label element. + node - A . + controller - A configuration/controller object passed to the visualization. + + */ + placeLabel: function(tag, node, controller) { + var pos = node.pos.getc(true), dim = this.node, canvas = this.viz.canvas; + var w = dim.overridable && node.data.$width || dim.width; + var h = dim.overridable && node.data.$height || dim.height; + var radius = canvas.getSize(); + var labelPos, orn; + if(dim.align == "center") { + labelPos= { + x: Math.round(pos.x - w / 2 + radius.width/2), + y: Math.round(pos.y - h / 2 + radius.height/2) + }; + } else if (dim.align == "left") { + orn = this.config.orientation; + if(orn == "bottom" || orn == "top") { + labelPos= { + x: Math.round(pos.x - w / 2 + radius.width/2), + y: Math.round(pos.y + radius.height/2) + }; + } else { + labelPos= { + x: Math.round(pos.x + radius.width/2), + y: Math.round(pos.y - h / 2 + radius.height/2) + }; + } + } else if(dim.align == "right") { + orn = this.config.orientation; + if(orn == "bottom" || orn == "top") { + labelPos= { + x: Math.round(pos.x - w / 2 + radius.width/2), + y: Math.round(pos.y - h + radius.height/2) + }; + } else { + labelPos= { + x: Math.round(pos.x - w + radius.width/2), + y: Math.round(pos.y - h / 2 + radius.height/2) + }; + } + } else throw "align: not implemented"; + + var style = tag.style; + style.left = labelPos.x + 'px'; + style.top = labelPos.y + 'px'; + style.display = this.fitsInCanvas(labelPos, canvas)? '' : 'none'; + controller.onPlaceLabel(tag, node); + }, + + + getAlignedPos: function(pos, width, height) { + var nconfig = this.node; + var square, orn; + if(nconfig.align == "center") { + square = { + x: pos.x - width / 2, + y: pos.y - height / 2 + }; + } else if (nconfig.align == "left") { + orn = this.config.orientation; + if(orn == "bottom" || orn == "top") { + square = { + x: pos.x - width / 2, + y: pos.y + }; + } else { + square = { + x: pos.x, + y: pos.y - height / 2 + }; + } + } else if(nconfig.align == "right") { + orn = this.config.orientation; + if(orn == "bottom" || orn == "top") { + square = { + x: pos.x - width / 2, + y: pos.y - height + }; + } else { + square = { + x: pos.x - width, + y: pos.y - height / 2 + }; + } + } else throw "align: not implemented"; + + return square; + }, + + getOrientation: function(adj) { + var config = this.config; + var orn = config.orientation; + + if(config.multitree) { + var nodeFrom = adj.nodeFrom; + var nodeTo = adj.nodeTo; + orn = (('$orn' in nodeFrom.data) + && nodeFrom.data.$orn) + || (('$orn' in nodeTo.data) + && nodeTo.data.$orn); + } + + return orn; + } +}); + +/* + Class: ST.Plot.NodeTypes + + Here are implemented all kinds of node rendering functions. + Rendering functions implemented are 'none', 'circle', 'ellipse', 'rectangle' and 'square'. + + You can add new Node types by implementing a new method in this class + + Example: + + (start code js) + ST.Plot.NodeTypes.implement({ + 'newnodetypename': function(node, canvas) { + //Render my node here. + } + }); + (end code) + +*/ +ST.Plot.NodeTypes = new Class({ + 'none': function() {}, + + 'circle': function(node, canvas) { + var pos = node.pos.getc(true), nconfig = this.node, data = node.data; + var cond = nconfig.overridable && data; + var dim = cond && data.$dim || nconfig.dim; + var algnPos = this.getAlignedPos(pos, dim * 2, dim * 2); + canvas.path('fill', function(context) { + context.arc(algnPos.x + dim, algnPos.y + dim, dim, 0, Math.PI * 2, true); + }); + }, + + 'square': function(node, canvas) { + var pos = node.pos.getc(true), nconfig = this.node, data = node.data; + var cond = nconfig.overridable && data; + var dim = cond && data.$dim || nconfig.dim; + var algnPos = this.getAlignedPos(pos, dim, dim); + canvas.getCtx().fillRect(algnPos.x, algnPos.y, dim, dim); + }, + + 'ellipse': function(node, canvas) { + var pos = node.pos.getc(true), nconfig = this.node, data = node.data; + var cond = nconfig.overridable && data; + var width = (cond && data.$width || nconfig.width) / 2; + var height = (cond && data.$height || nconfig.height) / 2; + var algnPos = this.getAlignedPos(pos, width * 2, height * 2); + var ctx = canvas.getCtx(); + ctx.save(); + ctx.scale(width / height, height / width); + canvas.path('fill', function(context) { + context.arc((algnPos.x + width) * (height / width), (algnPos.y + height) * (width / height), height, 0, Math.PI * 2, true); + }); + ctx.restore(); + }, + + 'rectangle': function(node, canvas) { + var pos = node.pos.getc(true), nconfig = this.node, data = node.data; + var cond = nconfig.overridable && data; + var width = cond && data.$width || nconfig.width; + var height = cond && data.$height || nconfig.height; + var algnPos = this.getAlignedPos(pos, width, height); + canvas.getCtx().fillRect(algnPos.x, algnPos.y, width, height); + } +}); + +/* + Class: ST.Plot.EdgeTypes + + Here are implemented all kinds of edge rendering functions. + Rendering functions implemented are 'none', 'line', 'quadratic:begin', 'quadratic:end', 'bezier' and 'arrow'. + + You can add new Edge types by implementing a new method in this class + + Example: + + (start code js) + ST.Plot.EdgeTypes.implement({ + 'newedgetypename': function(adj, canvas) { + //Render my edge here. + } + }); + (end code) + +*/ +ST.Plot.EdgeTypes = new Class({ + 'none': function() {}, + + 'line': function(adj, canvas) { + var orn = this.getOrientation(adj); + var nodeFrom = adj.nodeFrom, nodeTo = adj.nodeTo; + var begin = this.viz.geom.getEdge(nodeFrom._depth < nodeTo._depth? nodeFrom:nodeTo, 'begin', orn); + var end = this.viz.geom.getEdge(nodeFrom._depth < nodeTo._depth? nodeTo:nodeFrom, 'end', orn); + canvas.path('stroke', function(ctx) { + ctx.moveTo(begin.x, begin.y); + ctx.lineTo(end.x, end.y); + }); + }, + + 'quadratic:begin': function(adj, canvas) { + var orn = this.getOrientation(adj); + var data = adj.data, econfig = this.edge; + var nodeFrom = adj.nodeFrom, nodeTo = adj.nodeTo; + var begin = this.viz.geom.getEdge(nodeFrom._depth < nodeTo._depth? nodeFrom:nodeTo, 'begin', orn); + var end = this.viz.geom.getEdge(nodeFrom._depth < nodeTo._depth? nodeTo:nodeFrom, 'end', orn); + var cond = econfig.overridable && data; + var dim = cond && data.$dim || econfig.dim; + switch(orn) { + case "left": + canvas.path('stroke', function(ctx){ + ctx.moveTo(begin.x, begin.y); + ctx.quadraticCurveTo(begin.x + dim, begin.y, end.x, end.y); + }); + break; + case "right": + canvas.path('stroke', function(ctx){ + ctx.moveTo(begin.x, begin.y); + ctx.quadraticCurveTo(begin.x - dim, begin.y, end.x, end.y); + }); + break; + case "top": + canvas.path('stroke', function(ctx){ + ctx.moveTo(begin.x, begin.y); + ctx.quadraticCurveTo(begin.x, begin.y + dim, end.x, end.y); + }); + break; + case "bottom": + canvas.path('stroke', function(ctx){ + ctx.moveTo(begin.x, begin.y); + ctx.quadraticCurveTo(begin.x, begin.y - dim, end.x, end.y); + }); + break; + } + }, + + 'quadratic:end': function(adj, canvas) { + var orn = this.getOrientation(adj); + var data = adj.data, econfig = this.edge; + var nodeFrom = adj.nodeFrom, nodeTo = adj.nodeTo; + var begin = this.viz.geom.getEdge(nodeFrom._depth < nodeTo._depth? nodeFrom:nodeTo, 'begin', orn); + var end = this.viz.geom.getEdge(nodeFrom._depth < nodeTo._depth? nodeTo:nodeFrom, 'end', orn); + var cond = econfig.overridable && data; + var dim = cond && data.$dim || econfig.dim; + switch(orn) { + case "left": + canvas.path('stroke', function(ctx){ + ctx.moveTo(begin.x, begin.y); + ctx.quadraticCurveTo(end.x - dim, end.y, end.x, end.y); + }); + break; + case "right": + canvas.path('stroke', function(ctx){ + ctx.moveTo(begin.x, begin.y); + ctx.quadraticCurveTo(end.x + dim, end.y, end.x, end.y); + }); + break; + case "top": + canvas.path('stroke', function(ctx){ + ctx.moveTo(begin.x, begin.y); + ctx.quadraticCurveTo(end.x, end.y - dim, end.x, end.y); + }); + break; + case "bottom": + canvas.path('stroke', function(ctx){ + ctx.moveTo(begin.x, begin.y); + ctx.quadraticCurveTo(end.x, end.y + dim, end.x, end.y); + }); + break; + } + }, + + 'bezier': function(adj, canvas) { + var data = adj.data, econfig = this.edge; + var orn = this.getOrientation(adj); + var nodeFrom = adj.nodeFrom, nodeTo = adj.nodeTo; + var begin = this.viz.geom.getEdge(nodeFrom._depth < nodeTo._depth? nodeFrom:nodeTo, 'begin', orn); + var end = this.viz.geom.getEdge(nodeFrom._depth < nodeTo._depth? nodeTo:nodeFrom, 'end', orn); + var cond = econfig.overridable && data; + var dim = cond && data.$dim || econfig.dim; + switch(orn) { + case "left": + canvas.path('stroke', function(ctx) { + ctx.moveTo(begin.x, begin.y); + ctx.bezierCurveTo(begin.x + dim, begin.y, end.x - dim, end.y, end.x, end.y); + }); + break; + case "right": + canvas.path('stroke', function(ctx) { + ctx.moveTo(begin.x, begin.y); + ctx.bezierCurveTo(begin.x - dim, begin.y, end.x + dim, end.y, end.x, end.y); + }); + break; + case "top": + canvas.path('stroke', function(ctx) { + ctx.moveTo(begin.x, begin.y); + ctx.bezierCurveTo(begin.x, begin.y + dim, end.x, end.y - dim, end.x, end.y); + }); + break; + case "bottom": + canvas.path('stroke', function(ctx) { + ctx.moveTo(begin.x, begin.y); + ctx.bezierCurveTo(begin.x, begin.y - dim, end.x, end.y + dim, end.x, end.y); + }); + break; + } + }, + + 'arrow': function(adj, canvas) { + var orn = this.getOrientation(adj); + var node = adj.nodeFrom, child = adj.nodeTo; + var data = adj.data, econfig = this.edge; + //get edge dim + var cond = econfig.overridable && data; + var edgeDim = cond && data.$dim || econfig.dim; + //get edge direction + if(cond && data.$direction && data.$direction.length > 1) { + var nodeHash = {}; + nodeHash[node.id] = node; + nodeHash[child.id] = child; + var sense = data.$direction; + node = nodeHash[sense[0]]; + child = nodeHash[sense[1]]; + } + var posFrom = this.viz.geom.getEdge(node, 'begin', orn); + var posTo = this.viz.geom.getEdge(child, 'end', orn); + var vect = new Complex(posTo.x - posFrom.x, posTo.y - posFrom.y); + vect.$scale(edgeDim / vect.norm()); + var intermediatePoint = new Complex(posTo.x - vect.x, posTo.y - vect.y); + var normal = new Complex(-vect.y / 2, vect.x / 2); + var v1 = intermediatePoint.add(normal), v2 = intermediatePoint.$add(normal.$scale(-1)); + canvas.path('stroke', function(context) { + context.moveTo(posFrom.x, posFrom.y); + context.lineTo(posTo.x, posTo.y); + }); + canvas.path('fill', function(context) { + context.moveTo(v1.x, v1.y); + context.lineTo(v2.x, v2.y); + context.lineTo(posTo.x, posTo.y); + }); + } +}); + + +})(); + + + +/* + * File: AngularWidth.js + * + * Provides utility methods for calculating angular widths. + * + * Implemented by: + * + * , + * + */ + +/* + Object: AngularWidth + + Provides utility methods for calculating angular widths. +*/ +var AngularWidth = { + /* + Method: setAngularWidthForNodes + + Sets nodes angular widths. + */ + setAngularWidthForNodes: function() { + var config = this.config.Node; + var overridable = config.overridable; + var dim = config.dim; + + Graph.Util.eachBFS(this.graph, this.root, function(elem, i) { + var diamValue = (overridable + && elem.data + && elem.data.$aw) || dim; + elem._angularWidth = diamValue / i; + }, "ignore"); + }, + + /* + Method: setSubtreesAngularWidth + + Sets subtrees angular widths. + */ + setSubtreesAngularWidth: function() { + var that = this; + Graph.Util.eachNode(this.graph, function(elem) { + that.setSubtreeAngularWidth(elem); + }, "ignore"); + }, + + /* + Method: setSubtreeAngularWidth + + Sets the angular width for a subtree. + */ + setSubtreeAngularWidth: function(elem) { + var that = this, nodeAW = elem._angularWidth, sumAW = 0; + Graph.Util.eachSubnode(elem, function(child) { + that.setSubtreeAngularWidth(child); + sumAW += child._treeAngularWidth; + }, "ignore"); + elem._treeAngularWidth = Math.max(nodeAW, sumAW); + }, + + /* + Method: computeAngularWidths + + Computes nodes and subtrees angular widths. + */ + computeAngularWidths: function () { + this.setAngularWidthForNodes(); + this.setSubtreesAngularWidth(); + } + +}; + + +/* + * File: RGraph.js + * + * Implements the class and other derived classes. + * + * Description: + * + * A radial layout of a tree puts the root node on the center of the canvas, places its children on the first concentric ring away from the root node, its grandchildren on a second concentric ring, and so on... + * + * Ka-Ping Yee, Danyel Fisher, Rachna Dhamija and Marti Hearst introduced a very interesting paper called "Animated Exploration of Dynamic Graphs with Radial Layout". In this paper they describe a way to animate a radial layout of a tree with ease-in and ease-out transitions, which make transitions from a graph's state to another easier to understand for the viewer. + * + * Inspired by: + * + * Animated Exploration of Dynamic Graphs with Radial Layout (Ka-Ping Yee, Danyel Fisher, Rachna Dhamija, Marti Hearst) + * + * + * + * Disclaimer: + * + * This visualization was built from scratch, taking only the paper as inspiration, and only shares some features with this paper. + * + * + */ + +/* + Class: RGraph + + The main RGraph class + + Extends: + + , + + Parameters: + + canvas - A Class + config - A configuration/controller object. + + Configuration: + + The configuration object can have the following properties (all properties are optional and have a default value) + + *General* + + - _interpolation_ Interpolation type used for animations. Possible options are 'polar' and 'linear'. Default's 'linear'. + - _levelDistance_ Distance between a parent node and its children. Default's 100. + - _withLabels_ Whether the visualization should use/create labels or not. Default's *true*. + + *Node* + + Customize the visualization nodes' shape, color, and other style properties. + + - _Node_ + + This object has as properties + + - _overridable_ Determine whether or not nodes properties can be overriden by a particular node. Default's false. + + If given a JSON tree or graph, a node _data_ property contains properties which are the same as defined here but prefixed with + a dollar sign (i.e $), the node properties will override the global node properties. + + - _type_ Node type (shape). Possible options are "none", "square", "rectangle", "circle", "triangle", "star". Default's "circle". + - _color_ Node color. Default's '#ccb'. + - _lineWidth_ Line width. If nodes aren't drawn with strokes then this property won't be of any use. Default's 1. + - _height_ Node height. Used for plotting rectangular nodes. Default's 5. + - _width_ Node width. Used for plotting rectangular nodes. Default's 5. + - _dim_ An extra parameter used by other complex shapes such as square and circle to determine the shape's diameter. Default's 3. + + *Edge* + + Customize the visualization edges' shape, color, and other style properties. + + - _Edge_ + + This object has as properties + + - _overridable_ Determine whether or not edges properties can be overriden by a particular edge object. Default's false. + + If given a JSON _complex_ graph (defined in ), an adjacency _data_ property contains properties which are the same as defined here but prefixed with + a dollar sign (i.e $), the adjacency properties will override the global edge properties. + + - _type_ Edge type (shape). Possible options are "none", "line" and "arrow". Default's "line". + - _color_ Edge color. Default's '#ccb'. + - _lineWidth_ Line width. If edges aren't drawn with strokes then this property won't be of any use. Default's 1. + + *Animations* + + - _duration_ Duration of the animation in milliseconds. Default's 2500. + - _fps_ Frames per second. Default's 40. + - _transition_ One of the transitions defined in the class. Default's Quart.easeInOut. + - _clearCanvas_ Whether to clear canvas on each animation frame or not. Default's true. + + *Controller options* + + You can also implement controller functions inside the configuration object. This functions are + + - _onBeforeCompute(node)_ This method is called right before performing all computation and animations to the JIT visualization. + - _onAfterCompute()_ This method is triggered right after all animations or computations for the JIT visualizations ended. + - _onCreateLabel(domElement, node)_ This method receives the label dom element as first parameter, and the corresponding as second parameter. This method will only be called on label creation. Note that a is a node from the input tree/graph you provided to the visualization. If you want to know more about what kind of JSON tree/graph format is used to feed the visualizations, you can take a look at . This method proves useful when adding events to the labels used by the JIT. + - _onPlaceLabel(domElement, node)_ This method receives the label dom element as first parameter and the corresponding as second parameter. This method is called each time a label has been placed on the visualization, and thus it allows you to update the labels properties, such as size or position. Note that onPlaceLabel will be triggered after updating the labels positions. That means that, for example, the left and top css properties are already updated to match the nodes positions. + - _onBeforePlotNode(node)_ This method is triggered right before plotting a given node. The _node_ parameter is the to be plotted. +This method is useful for changing a node style right before plotting it. + - _onAfterPlotNode(node)_ This method is triggered right after plotting a given node. The _node_ parameter is the plotted. + - _onBeforePlotLine(adj)_ This method is triggered right before plotting an edge. The _adj_ parameter is a object. +This method is useful for adding some styles to a particular edge before being plotted. + - _onAfterPlotLine(adj)_ This method is triggered right after plotting an edge. The _adj_ parameter is the plotted. + + Example: + + Here goes a complete example. In most cases you won't be forced to implement all properties and methods. In fact, + all configuration properties have the default value assigned. + + I won't be instanciating a class here. If you want to know more about instanciating a class + please take a look at the class documentation. + + (start code js) + var rgraph = new RGraph(canvas, { + interpolation: 'linear', + levelDistance: 100, + withLabels: true, + Node: { + overridable: false, + type: 'circle', + color: '#ccb', + lineWidth: 1, + height: 5, + width: 5, + dim: 3 + }, + Edge: { + overridable: false, + type: 'line', + color: '#ccb', + lineWidth: 1 + }, + duration: 2500, + fps: 40, + transition: Trans.Quart.easeInOut, + clearCanvas: true, + onBeforeCompute: function(node) { + //do something onBeforeCompute + }, + onAfterCompute: function () { + //do something onAfterCompute + }, + onCreateLabel: function(domElement, node) { + //do something onCreateLabel + }, + onPlaceLabel: function(domElement, node) { + //do something onPlaceLabel + }, + onBeforePlotNode:function(node) { + //do something onBeforePlotNode + }, + onAfterPlotNode: function(node) { + //do something onAfterPlotNode + }, + onBeforePlotLine:function(adj) { + //do something onBeforePlotLine + }, + onAfterPlotLine: function(adj) { + //do something onAfterPlotLine + } + }); + (end code) + + Instance Properties: + + - _graph_ Access a instance. + - _op_ Access a instance. + - _fx_ Access a instance. +*/ + +this.RGraph = new Class({ + + Implements: [Loader, AngularWidth], + + initialize: function(canvas, controller) { + var config= { + labelContainer: canvas.id + '-label', + + interpolation: 'linear', + levelDistance: 100, + withLabels: true, + + Node: { + overridable: false, + type: 'circle', + dim: 3, + color: '#ccb', + width: 5, + height: 5, + lineWidth: 1 + }, + + Edge: { + overridable: false, + type: 'line', + color: '#ccb', + lineWidth: 1 + }, + + fps:40, + duration: 2500, + transition: Trans.Quart.easeInOut, + clearCanvas: true + }; + + var innerController = { + onBeforeCompute: $empty, + onAfterCompute: $empty, + onCreateLabel: $empty, + onPlaceLabel: $empty, + onComplete: $empty, + onBeforePlotLine:$empty, + onAfterPlotLine: $empty, + onBeforePlotNode:$empty, + onAfterPlotNode: $empty + }; + + this.controller = this.config = $merge(config, innerController, controller); + this.graphOptions = { + 'complex': false, + 'Node': { + 'selected': false, + 'exist': true, + 'drawn': true + } + }; + this.graph = new Graph(this.graphOptions); + this.fx = new RGraph.Plot(this); + this.op = new RGraph.Op(this); + this.json = null; + this.canvas = canvas; + this.root = null; + this.busy = false; + this.parent = false; + }, + /* + Method: refresh + + Computes nodes' positions and replots the tree. + + */ + refresh: function() { + this.compute(); + this.plot(); + }, + + /* + Method: reposition + + An alias for computing new positions to _endPos_ + + See also: + + + + */ + reposition: function() { + this.compute('endPos'); + }, + + + /* + Method: plot + + Plots the RGraph + */ + plot: function() { + this.fx.plot(); + }, + /* + Method: compute + + Computes nodes' positions. + + Parameters: + + property - _optional_ A position property to store the new positions. Possible values are 'pos', 'endPos' or 'startPos'. + + */ + compute: function(property) { + var prop = property || ['pos', 'startPos', 'endPos']; + var node = this.graph.getNode(this.root); + node._depth = 0; + Graph.Util.computeLevels(this.graph, this.root, 0, "ignore"); + this.computeAngularWidths(); + this.computePositions(prop); + }, + + /* + computePositions + + Performs the main algorithm for computing node positions. + */ + computePositions: function(property) { + var propArray = $splat(property); + var aGraph = this.graph; + var GUtil = Graph.Util; + var root = this.graph.getNode(this.root); + var parent = this.parent; + var config = this.config; + + for(var i=0; i 0 && subnodes[0].dist) { + subnodes.sort(function(a, b) { + return (a.dist >= b.dist) - (a.dist <= b.dist); + }); + } + for(var k=0; k < subnodes.length; k++) { + var child = subnodes[k]; + if(!child._flag) { + child._rel = child._treeAngularWidth / totalAngularWidths; + var angleProportion = child._rel * angleSpan; + var theta = angleInit + angleProportion / 2; + + for(var i=0; i 0)? ps[0] : false; + if(p) { + var posParent = p.pos.getc(), posChild = n.pos.getc(); + var newPos = posParent.add(posChild.scale(-1)); + theta = Math.atan2(newPos.y, newPos.x); + if(theta < 0) theta += 2 * Math.PI; + } + return {parent: p, theta: theta}; + }, + + /* + tagChildren + + Enumerates the children in order to mantain child ordering (second constraint of the paper). + */ + tagChildren: function(par, id) { + if(par.angleSpan) { + var adjs = []; + Graph.Util.eachAdjacency(par, function(elem) { + adjs.push(elem.nodeTo); + }, "ignore"); + var len = adjs.length; + for(var i=0; i < len && id != adjs[i].id; i++); + for(var j= (i+1) % len, k = 0; id != adjs[j].id; j = (j+1) % len) { + adjs[j].dist = k++; + } + } + }, + + /* + Method: onClick + + Performs all calculations and animations to center the node specified by _id_. + + Parameters: + + id - A id. + opt - _optional_ An object containing some extra properties like + + - _hideLabels_ Hide labels when performing the animation. Default's *true*. + + Example: + + (start code js) + rgraph.onClick('someid'); + //or also... + rgraph.onClick('someid', { + hideLabels: false + }); + (end code) + + */ + onClick: function(id, opt) { + if(this.root != id && !this.busy) { + this.busy = true; + this.root = id; + that = this; + this.controller.onBeforeCompute(this.graph.getNode(id)); + var obj = this.getNodeAndParentAngle(id); + + //second constraint + this.tagChildren(obj.parent, id); + this.parent = obj.parent; + this.compute('endPos'); + + //first constraint + var thetaDiff = obj.theta - obj.parent.endPos.theta; + Graph.Util.eachNode(this.graph, function(elem) { + elem.endPos.set(elem.endPos.getp().add($P(thetaDiff, 0))); + }); + + var mode = this.config.interpolation; + opt = $merge({ onComplete: $empty }, opt || {}); + + this.fx.animate($merge({ + hideLabels: true, + modes: [mode] + }, opt, { + onComplete: function() { + that.busy = false; + opt.onComplete(); + } + })); + } + } +}); + +/* + Class: RGraph.Op + + Performs advanced operations on trees and graphs. + + Extends: + + All methods + + Access: + + This instance can be accessed with the _op_ parameter of the instance created. + + Example: + + (start code js) + var rgraph = new RGraph(canvas, config); + rgraph.op.morph //or can also call any other method + (end code) + +*/ +RGraph.Op = new Class({ + + Implements: Graph.Op, + + initialize: function(viz) { + this.viz = viz; + } +}); + +/* + Class: RGraph.Plot + + Performs plotting operations. + + Extends: + + All methods + + Access: + + This instance can be accessed with the _fx_ parameter of the instance created. + + Example: + + (start code js) + var rgraph = new RGraph(canvas, config); + rgraph.fx.placeLabel //or can also call any other method + (end code) + +*/ +RGraph.Plot = new Class({ + + Implements: Graph.Plot, + + initialize: function(viz) { + this.viz = viz; + this.config = viz.config; + this.node = viz.config.Node; + this.edge = viz.config.Edge; + this.animation = new Animation; + this.nodeTypes = new RGraph.Plot.NodeTypes; + this.edgeTypes = new RGraph.Plot.EdgeTypes; + }, + + /* + Method: placeLabel + + Overrides abstract method placeLabel in . + + Parameters: + + tag - A DOM label element. + node - A . + controller - A configuration/controller object passed to the visualization. + + */ + placeLabel: function(tag, node, controller) { + var pos = node.pos.getc(true), canvas = this.viz.canvas; + var radius= canvas.getSize(); + var labelPos= { + x: Math.round(pos.x + radius.width/2), + y: Math.round(pos.y + radius.height/2) + }; + var style = tag.style; + style.left = labelPos.x + 'px'; + style.top = labelPos.y + 'px'; + style.display = this.fitsInCanvas(labelPos, canvas)? '' : 'none'; + controller.onPlaceLabel(tag, node); + } +}); + +/* + Class: RGraph.Plot.NodeTypes + + Here are implemented all kinds of node rendering functions. + Rendering functions implemented are 'none', 'circle', 'triangle', 'rectangle', 'star' and 'square'. + + You can add new Node types by implementing a new method in this class + + Example: + + (start code js) + RGraph.Plot.NodeTypes.implement({ + 'newnodetypename': function(node, canvas) { + //Render my node here. + } + }); + (end code) + +*/ +RGraph.Plot.NodeTypes = new Class({ + 'none': function() {}, + + 'circle': function(node, canvas) { + var pos = node.pos.getc(true), nconfig = this.node, data = node.data; + var nodeDim = nconfig.overridable && data && data.$dim || nconfig.dim; + canvas.path('fill', function(context) { + context.arc(pos.x, pos.y, nodeDim, 0, Math.PI*2, true); + }); + }, + + 'square': function(node, canvas) { + var pos = node.pos.getc(true), nconfig = this.node, data = node.data; + var nodeDim = nconfig.overridable && data && data.$dim || nconfig.dim; + var nodeDim2 = 2 * nodeDim; + canvas.getCtx().fillRect(pos.x - nodeDim, pos.y - nodeDim, nodeDim2, nodeDim2); + }, + + 'rectangle': function(node, canvas) { + var pos = node.pos.getc(true), nconfig = this.node, data = node.data; + var width = nconfig.overridable && data && data.$width || nconfig.width; + var height = nconfig.overridable && data && data.$height || nconfig.height; + canvas.getCtx().fillRect(pos.x - width / 2, pos.y - height / 2, width, height); + }, + + 'triangle': function(node, canvas) { + var pos = node.pos.getc(true), nconfig = this.node, data = node.data; + var nodeDim = nconfig.overridable && data && data.$dim || nconfig.dim; + var c1x = pos.x, c1y = pos.y - nodeDim, + c2x = c1x - nodeDim, c2y = pos.y + nodeDim, + c3x = c1x + nodeDim, c3y = c2y; + canvas.path('fill', function(ctx) { + ctx.moveTo(c1x, c1y); + ctx.lineTo(c2x, c2y); + ctx.lineTo(c3x, c3y); + }); + }, + + 'star': function(node, canvas) { + var pos = node.pos.getc(true), nconfig = this.node, data = node.data; + var nodeDim = nconfig.overridable && data && data.$dim || nconfig.dim; + var ctx = canvas.getCtx(), pi5 = Math.PI / 5; + ctx.save(); + ctx.translate(pos.x, pos.y); + ctx.beginPath(); + ctx.moveTo(nodeDim, 0); + for (var i=0; i<9; i++){ + ctx.rotate(pi5); + if(i % 2 == 0) { + ctx.lineTo((nodeDim / 0.525731) * 0.200811, 0); + } else { + ctx.lineTo(nodeDim, 0); + } + } + ctx.closePath(); + ctx.fill(); + ctx.restore(); + } +}); + +/* + Class: RGraph.Plot.EdgeTypes + + Here are implemented all kinds of edge rendering functions. + Rendering functions implemented are 'none', 'line' and 'arrow'. + + You can add new Edge types by implementing a new method in this class + + Example: + + (start code js) + RGraph.Plot.EdgeTypes.implement({ + 'newedgetypename': function(adj, canvas) { + //Render my edge here. + } + }); + (end code) + +*/ +RGraph.Plot.EdgeTypes = new Class({ + 'none': function() {}, + + 'line': function(adj, canvas) { + var pos = adj.nodeFrom.pos.getc(true); + var posChild = adj.nodeTo.pos.getc(true); + canvas.path('stroke', function(context) { + context.moveTo(pos.x, pos.y); + context.lineTo(posChild.x, posChild.y); + }); + }, + + 'arrow': function(adj, canvas) { + var node = adj.nodeFrom, child = adj.nodeTo; + var data = adj.data, econfig = this.edge; + //get edge dim + var cond = econfig.overridable && data; + var edgeDim = cond && data.$dim || 14; + //get edge direction + if(cond && data.$direction && data.$direction.length > 1) { + var nodeHash = {}; + nodeHash[node.id] = node; + nodeHash[child.id] = child; + var sense = data.$direction; + node = nodeHash[sense[0]]; + child = nodeHash[sense[1]]; + } + var posFrom = node.pos.getc(true), posTo = child.pos.getc(true); + var vect = new Complex(posTo.x - posFrom.x, posTo.y - posFrom.y); + vect.$scale(edgeDim / vect.norm()); + var intermediatePoint = new Complex(posTo.x - vect.x, posTo.y - vect.y); + var normal = new Complex(-vect.y / 2, vect.x / 2); + var v1 = intermediatePoint.add(normal), v2 = intermediatePoint.$add(normal.$scale(-1)); + canvas.path('stroke', function(context) { + context.moveTo(posFrom.x, posFrom.y); + context.lineTo(posTo.x, posTo.y); + }); + canvas.path('fill', function(context) { + context.moveTo(v1.x, v1.y); + context.lineTo(v2.x, v2.y); + context.lineTo(posTo.x, posTo.y); + }); + } +}); + + +/* + * File: Hypertree.js + * + * Implements the class and other derived classes. + * + * Description: + * + * A Hyperbolic Tree (HT) is a focus+context information visualization technique used to display large amount of inter-related data. This technique was originally developed at Xerox PARC. + * + * The HT algorithm plots a tree in what's called the Poincare Disk model of Hyperbolic Geometry, a kind of non-Euclidean geometry. By doing this, the HT algorithm applies a moebius transformation to the tree in order to display it with a magnifying glass effect. + * + * Inspired by: + * + * A Focus+Context Technique Based on Hyperbolic Geometry for Visualizing Large Hierarchies (John Lamping, Ramana Rao, and Peter Pirolli). + * + * + * + * Disclaimer: + * + * This visualization was built from scratch, taking only the paper as inspiration, and only shares some features with the Hypertree. + * + +*/ + +/* + Complex + + A multi-purpose Complex Class with common methods. Exetended for the Hypertree. + +*/ +/* + moebiusTransformation + + Calculates a moebius transformation for this point / complex. + For more information go to: + http://en.wikipedia.org/wiki/Moebius_transformation. + + Parameters: + + c - An initialized Complex instance representing a translation Vector. +*/ + +Complex.prototype.moebiusTransformation = function(c) { + var num = this.add(c); + var den = c.$conjugate().$prod(this); den.x++; + return num.$div(den); +}; + +/* + Method: getClosestNodeToOrigin + + Extends . Returns the closest node to the center of canvas. + + Parameters: + + graph - A instance. + prop - _optional_ a position property. Possible properties are 'startPos', 'pos' or 'endPos'. Default's 'pos'. + + Returns: + + Closest node to origin. Returns *null* otherwise. + +*/ +Graph.Util.getClosestNodeToOrigin = function(graph, prop, flags) { + return this.getClosestNodeToPos(graph, Polar.KER, prop, flags); +}; + +/* + Method: getClosestNodeToPos + + Extends . Returns the closest node to the given position. + + Parameters: + + graph - A instance. + p[os - A or instance. + prop - _optional_ a position property. Possible properties are 'startPos', 'pos' or 'endPos'. Default's 'pos'. + + Returns: + + Closest node to the given position. Returns *null* otherwise. + +*/ +Graph.Util.getClosestNodeToPos = function(graph, pos, prop, flags) { + var node = null; prop = prop || 'pos'; pos = pos && pos.getc(true) || Complex.KER; + var distance = function(a, b) { + var d1 = a.x - b.x, d2 = a.y - b.y; + return d1 * d1 + d2 * d2; + }; + this.eachNode(graph, function(elem) { + node = (node == null || distance(elem[prop].getc(true), pos) < distance(node[prop].getc(true), pos))? elem : node; + }, flags); + return node; +}; + +/* + moebiusTransformation + + Calculates a moebius transformation for the hyperbolic tree. + + + + Parameters: + + graph - A instance. + pos - A . + prop - A property array. + theta - Rotation angle. + startPos - _optional_ start position. +*/ +Graph.Util.moebiusTransformation = function(graph, pos, prop, startPos, flags) { + this.eachNode(graph, function(elem) { + for(var i=0; i, + + Parameters: + + canvas - A Class + config - A configuration/controller object. + + Configuration: + + The configuration object can have the following properties (all properties are optional and have a default value) + + *General* + - _withLabels_ Whether the visualization should use/create labels or not. Default's *true*. + + *Node* + + Customize the visualization nodes' shape, color, and other style properties. + + - _Node_ + + This object has as properties + + - _overridable_ Determine whether or not nodes properties can be overriden by a particular node. Default's false. + + If given a JSON tree or graph, a node _data_ property contains properties which are the same as defined here but prefixed with + a dollar sign (i.e $), the node properties will override the global node properties. + + - _type_ Node type (shape). Possible options are "none", "square", "rectangle", "circle", "triangle", "star". Default's "circle". + - _color_ Node color. Default's '#ccb'. + - _lineWidth_ Line width. If nodes aren't drawn with strokes then this property won't be of any use. Default's 1. + - _height_ Node height. Used for plotting rectangular nodes. Default's 5. + - _width_ Node width. Used for plotting rectangular nodes. Default's 5. + - _dim_ An extra parameter used by other complex shapes such as square and circle to determine the shape's diameter. Default's 7. + - _transform_ Whether to apply the moebius transformation to the nodes or not. Default's true. + + *Edge* + + Customize the visualization edges' shape, color, and other style properties. + + - _Edge_ + + This object has as properties + + - _overridable_ Determine whether or not edges properties can be overriden by a particular edge object. Default's false. + + If given a JSON _complex_ graph (defined in ), an adjacency _data_ property contains properties which are the same as defined here but prefixed with + a dollar sign (i.e $), the adjacency properties will override the global edge properties. + + - _type_ Edge type (shape). Possible options are "none", "line" and "hyperline". Default's "hyperline". + - _color_ Edge color. Default's '#ccb'. + - _lineWidth_ Line width. If edges aren't drawn with strokes then this property won't be of any use. Default's 1. + + *Animations* + + - _duration_ Duration of the animation in milliseconds. Default's 1500. + - _fps_ Frames per second. Default's 40. + - _transition_ One of the transitions defined in the class. Default's Quart.easeInOut. + - _clearCanvas_ Whether to clear canvas on each animation frame or not. Default's true. + + *Controller options* + + You can also implement controller functions inside the configuration object. This functions are + + - _onBeforeCompute(node)_ This method is called right before performing all computation and animations to the JIT visualization. + - _onAfterCompute()_ This method is triggered right after all animations or computations for the JIT visualizations ended. + - _onCreateLabel(domElement, node)_ This method receives the label dom element as first parameter, and the corresponding as second parameter. This method will only be called on label creation. Note that a is a node from the input tree/graph you provided to the visualization. If you want to know more about what kind of JSON tree/graph format is used to feed the visualizations, you can take a look at . This method proves useful when adding events to the labels used by the JIT. + - _onPlaceLabel(domElement, node)_ This method receives the label dom element as first parameter and the corresponding as second parameter. This method is called each time a label has been placed on the visualization, and thus it allows you to update the labels properties, such as size or position. Note that onPlaceLabel will be triggered after updating the labels positions. That means that, for example, the left and top css properties are already updated to match the nodes positions. + - _onBeforePlotNode(node)_ This method is triggered right before plotting a given node. The _node_ parameter is the to be plotted. +This method is useful for changing a node style right before plotting it. + - _onAfterPlotNode(node)_ This method is triggered right after plotting a given node. The _node_ parameter is the plotted. + - _onBeforePlotLine(adj)_ This method is triggered right before plotting an edge. The _adj_ parameter is a object. +This method is useful for adding some styles to a particular edge before being plotted. + - _onAfterPlotLine(adj)_ This method is triggered right after plotting an edge. The _adj_ parameter is the plotted. + + Example: + + Here goes a complete example. In most cases you won't be forced to implement all properties and methods. In fact, + all configuration properties have the default value assigned. + + I won't be instanciating a class here. If you want to know more about instanciating a class + please take a look at the class documentation. + + (start code js) + var ht = new Hypertree(canvas, { + + Node: { + overridable: false, + type: 'circle', + color: '#ccb', + lineWidth: 1, + height: 5, + width: 5, + dim: 7, + transform: true + }, + Edge: { + overridable: false, + type: 'hyperline', + color: '#ccb', + lineWidth: 1 + }, + duration: 1500, + fps: 40, + transition: Trans.Quart.easeInOut, + clearCanvas: true, + withLabels: true, + + onBeforeCompute: function(node) { + //do something onBeforeCompute + }, + onAfterCompute: function () { + //do something onAfterCompute + }, + onCreateLabel: function(domElement, node) { + //do something onCreateLabel + }, + onPlaceLabel: function(domElement, node) { + //do something onPlaceLabel + }, + onBeforePlotNode:function(node) { + //do something onBeforePlotNode + }, + onAfterPlotNode: function(node) { + //do something onAfterPlotNode + }, + onBeforePlotLine:function(adj) { + //do something onBeforePlotLine + }, + onAfterPlotLine: function(adj) { + //do something onAfterPlotLine + } + }); + (end code) + + Instance Properties: + + - _graph_ Access a instance. + - _op_ Access a instance. + - _fx_ Access a instance. +*/ + +this.Hypertree = new Class({ + + Implements: [Loader, AngularWidth], + + initialize: function(canvas, controller) { + + var config = { + labelContainer: canvas.id + '-label', + + withLabels: true, + + Node: { + overridable: false, + type: 'circle', + dim: 7, + color: '#ccb', + width: 5, + height: 5, + lineWidth: 1, + transform: true + }, + + Edge: { + overridable: false, + type: 'hyperline', + color: '#ccb', + lineWidth: 1 + }, + clearCanvas: true, + fps:40, + duration: 1500, + transition: Trans.Quart.easeInOut + }; + + var innerController = { + onBeforeCompute: $empty, + onAfterCompute: $empty, + onCreateLabel: $empty, + onPlaceLabel: $empty, + onComplete: $empty, + onBeforePlotLine:$empty, + onAfterPlotLine: $empty, + onBeforePlotNode:$empty, + onAfterPlotNode: $empty + }; + + this.controller = this.config = $merge(config, innerController, controller); + this.graphOptions = { + 'complex': false, + 'Node': { + 'selected': false, + 'exist': true, + 'drawn': true + } + }; + this.graph = new Graph(this.graphOptions); + this.fx = new Hypertree.Plot(this); + this.op = new Hypertree.Op(this); + this.json = null; + this.canvas = canvas; + + this.root = null; + this.busy = false; + }, + + /* + Method: refresh + + Computes nodes' positions and replots the tree. + + Parameters: + + reposition - _optional_ Set this to *true* to force repositioning. + + See also: + + + + */ + refresh: function(reposition) { + if(reposition) { + this.reposition(); + Graph.Util.eachNode(this.graph, function(node) { + node.startPos.rho = node.pos.rho = node.endPos.rho; + node.startPos.theta = node.pos.theta = node.endPos.theta; + }); + } else { + this.compute(); + } + this.plot(); + }, + + /* + Method: reposition + + Computes nodes' positions and restores the tree to its previous position. + + For calculating nodes' positions the root must be placed on its origin. This method does this + and then attemps to restore the hypertree to its previous position. + + */ + reposition: function() { + this.compute('endPos'); + var vector = this.graph.getNode(this.root).pos.getc().scale(-1); + Graph.Util.moebiusTransformation(this.graph, [vector], ['endPos'], 'endPos', "ignore"); + Graph.Util.eachNode(this.graph, function(node) { + if (node.ignore) { + node.endPos.rho = node.pos.rho; + node.endPos.theta = node.pos.theta; + } + }); + }, + + /* + Method: plot + + Plots the Hypertree + + */ + plot: function() { + this.fx.plot(); + }, + + /* + Method: compute + + Computes nodes' positions. + + Parameters: + + property - _optional_ A position property to store the new positions. Possible values are 'pos', 'endPos' or 'startPos'. + + + */ + compute: function(property) { + var prop = property || ['pos', 'startPos']; + var node = this.graph.getNode(this.root); + node._depth = 0; + Graph.Util.computeLevels(this.graph, this.root, 0, "ignore"); + this.computeAngularWidths(); + this.computePositions(prop); + }, + + /* + computePositions + + Performs the main algorithm for computing node positions. + + Parameters: + + property - A position property to store the new positions. Possible values are 'pos', 'endPos' or 'startPos'. + + */ + computePositions: function(property) { + var propArray = $splat(property); + var aGraph = this.graph, GUtil = Graph.Util; + var root = this.graph.getNode(this.root), that = this, config = this.config; + var size = this.canvas.getSize(); + var scale = Math.min(size.width, size.height)/ 2; + + + //Set default values for the root node + for(var i=0; i depth)? node._depth : depth; + node._scale = scale; + }, "ignore"); + for(var i=0.51; i<=1; i+=0.01) { + var valSeries = (function(a, n) { + return (1 - Math.pow(a, n)) / (1 - a); + })(i, depth + 1); + if(valSeries >= 2) return i - 0.01; + } + return 0.5; + })(); + + GUtil.eachBFS(this.graph, this.root, function (elem) { + var angleSpan = elem.angleSpan.end - elem.angleSpan.begin; + var angleInit = elem.angleSpan.begin; + var totalAngularWidths = (function (element){ + var total = 0; + GUtil.eachSubnode(element, function(sib) { + total += sib._treeAngularWidth; + }, "ignore"); + return total; + })(elem); + + for(var i=1, rho = 0, lenAcum = edgeLength, depth = elem._depth; i<=depth+1; i++) { + rho += lenAcum; + lenAcum *= edgeLength; + } + + GUtil.eachSubnode(elem, function(child) { + if(!child._flag) { + child._rel = child._treeAngularWidth / totalAngularWidths; + var angleProportion = child._rel * angleSpan; + var theta = angleInit + angleProportion / 2; + + for(var i=0; i id. + opt - _optional_ An object containing some extra properties like + + - _hideLabels_ Hide labels when performing the animation. Default's *true*. + + Example: + + (start code js) + ht.onClick('someid'); + //or also... + ht.onClick('someid', { + hideLabels: false + }); + (end code) + + */ + onClick: function(id, opt) { + var pos = this.graph.getNode(id).pos.getc(true); + this.move(pos, opt); + }, + + /* + Method: move + + Translates the tree to the given position. + + Parameters: + + pos - A number determining the position to move the tree to. + opt - _optional_ An object containing some extra properties defined in + + + */ + move: function(pos, opt) { + var versor = $C(pos.x, pos.y); + if(this.busy === false && versor.norm() < 1) { + var GUtil = Graph.Util; + this.busy = true; + var root = GUtil.getClosestNodeToPos(this.graph, versor), that = this; + GUtil.computeLevels(this.graph, root.id, 0); + this.controller.onBeforeCompute(root); + if (versor.norm() < 1) { + opt = $merge({ onComplete: $empty }, opt || {}); + this.fx.animate($merge({ + modes: ['moebius'], + hideLabels: true + }, opt, { + onComplete: function(){ + that.busy = false; + opt.onComplete(); + } + }), versor); + } + } + } +}); + +/* + Class: Hypertree.Op + + Performs advanced operations on trees and graphs. + + Extends: + + All methods + + Access: + + This instance can be accessed with the _op_ parameter of the hypertree instance created. + + Example: + + (start code js) + var ht = new Hypertree(canvas, config); + ht.op.morph //or can also call any other method + (end code) + +*/ +Hypertree.Op = new Class({ + + Implements: Graph.Op, + + initialize: function(viz) { + this.viz = viz; + } +}); + +/* + Class: Hypertree.Plot + + Performs plotting operations. + + Extends: + + All methods + + Access: + + This instance can be accessed with the _fx_ parameter of the hypertree instance created. + + Example: + + (start code js) + var ht = new Hypertree(canvas, config); + ht.fx.placeLabel //or can also call any other method + (end code) + +*/ +Hypertree.Plot = new Class({ + + Implements: Graph.Plot, + + initialize: function(viz) { + this.viz = viz; + this.config = viz.config; + this.node = this.config.Node; + this.edge = this.config.Edge; + this.animation = new Animation; + this.nodeTypes = new Hypertree.Plot.NodeTypes; + this.edgeTypes = new Hypertree.Plot.EdgeTypes; + }, + + /* + Method: hyperline + + Plots a hyperline between two nodes. A hyperline is an arc of a circle which is orthogonal to the main circle. + + Parameters: + + adj - A object. + canvas - A instance. + */ + hyperline: function(adj, canvas) { + var node = adj.nodeFrom, child = adj.nodeTo, data = adj.data; + var pos = node.pos.getc(), posChild = child.pos.getc(); + var centerOfCircle = this.computeArcThroughTwoPoints(pos, posChild); + var size = canvas.getSize(); + var scale = Math.min(size.width, size.height)/2; + if (centerOfCircle.a > 1000 || centerOfCircle.b > 1000 || centerOfCircle.ratio > 1000) { + canvas.path('stroke', function(ctx) { + ctx.moveTo(pos.x * scale, pos.y * scale); + ctx.lineTo(posChild.x * scale, posChild.y * scale); + }); + } else { + var angleBegin = Math.atan2(posChild.y - centerOfCircle.y, posChild.x - centerOfCircle.x); + var angleEnd = Math.atan2(pos.y - centerOfCircle.y, pos.x - centerOfCircle.x); + var sense = this.sense(angleBegin, angleEnd); + var context = canvas.getCtx(); + canvas.path('stroke', function(ctx) { + ctx.arc(centerOfCircle.x*scale, centerOfCircle.y*scale, centerOfCircle.ratio*scale, angleBegin, angleEnd, sense); + }); + } + }, + + /* + computeArcThroughTwoPoints + + Calculates the arc parameters through two points. + + More information in + + Parameters: + + p1 - A instance. + p2 - A instance. + + Returns: + + An object containing some arc properties. + */ + computeArcThroughTwoPoints: function(p1, p2) { + var aDen = (p1.x * p2.y - p1.y * p2.x), bDen = aDen; + var sq1 = p1.squaredNorm(), sq2 = p2.squaredNorm(); + //Fall back to a straight line + if (aDen == 0) return { x:0, y:0, ratio: 1001 }; + + var a = (p1.y * sq2 - p2.y * sq1 + p1.y - p2.y) / aDen; + var b = (p2.x * sq1 - p1.x * sq2 + p2.x - p1.x) / bDen; + var x = -a / 2; + var y = -b / 2; + var squaredRatio = (a * a + b * b) / 4 -1; + //Fall back to a straight line + if(squaredRatio < 0) return { x:0, y:0, ratio: 1001 }; + var ratio = Math.sqrt(squaredRatio); + var out= { + x: x, + y: y, + ratio: ratio, + a: a, + b: b + }; + + return out; + }, + + /* + sense + + Sets angle direction to clockwise (true) or counterclockwise (false). + + Parameters: + + angleBegin - Starting angle for drawing the arc. + angleEnd - The HyperLine will be drawn from angleBegin to angleEnd. + + Returns: + + A Boolean instance describing the sense for drawing the HyperLine. + */ + sense: function(angleBegin, angleEnd) { + return (angleBegin < angleEnd)? ((angleBegin + Math.PI > angleEnd)? false : true) : + ((angleEnd + Math.PI > angleBegin)? true : false); + }, + + + /* + Method: placeLabel + + Overrides abstract method placeLabel in . + + Parameters: + + tag - A DOM label element. + node - A . + controller - A configuration/controller object passed to the visualization. + + */ + placeLabel: function(tag, node, controller) { + var pos = node.pos.getc(true), canvas = this.viz.canvas; + var radius= canvas.getSize(); + var scale = node._scale; + var labelPos= { + x: Math.round(pos.x * scale + radius.width/2), + y: Math.round(pos.y * scale + radius.height/2) + }; + var style = tag.style; + style.left = labelPos.x + 'px'; + style.top = labelPos.y + 'px'; + style.display = ''; + controller.onPlaceLabel(tag, node); + } +}); + +/* + Class: Hypertree.Plot.NodeTypes + + Here are implemented all kinds of node rendering functions. + Rendering functions implemented are 'none', 'circle', 'triangle', 'rectangle', 'star' and 'square'. + + You can add new Node types by implementing a new method in this class + + Example: + + (start code js) + Hypertree.Plot.NodeTypes.implement({ + 'newnodetypename': function(node, canvas) { + //Render my node here. + } + }); + (end code) + +*/ +Hypertree.Plot.NodeTypes = new Class({ + 'none': function() {}, + + 'circle': function(node, canvas) { + var nconfig = this.node, data = node.data; + var nodeDim = nconfig.overridable && data && data.$dim || nconfig.dim; + var p = node.pos.getc(), pos = p.scale(node._scale); + var prod = nconfig.transform? nodeDim * (1 - p.squaredNorm()) : nodeDim; + if(prod >= nodeDim / 4) { + canvas.path('fill', function(context) { + context.arc(pos.x, pos.y, prod, 0, Math.PI * 2, true); + }); + } + }, + + 'square': function(node, canvas) { + var nconfig = this.node, data = node.data; + var nodeDim = nconfig.overridable && data && data.$dim || nconfig.dim; + var p = node.pos.getc(), pos = p.scale(node._scale); + var prod = nconfig.transform? nodeDim * (1 - p.squaredNorm()) : nodeDim; + var nodeDim2 = 2 * prod; + if (prod >= nodeDim / 4) { + canvas.getCtx().fillRect(pos.x - prod, pos.y - prod, nodeDim2, nodeDim2); + } + }, + + 'rectangle': function(node, canvas) { + var nconfig = this.node, data = node.data; + var width = nconfig.overridable && data && data.$width || nconfig.width; + var height = nconfig.overridable && data && data.$height || nconfig.height; + var p = node.pos.getc(), pos = p.scale(node._scale); + var prod = 1 - p.squaredNorm(); + width = nconfig.transform? width * prod : width; + height = nconfig.transform? height * prod : height; + if(prod >= 0.25) { + canvas.getCtx().fillRect(pos.x - width / 2, pos.y - height / 2, width, height); + } + + }, + + 'triangle': function(node, canvas) { + var nconfig = this.node, data = node.data; + var nodeDim = nconfig.overridable && data && data.$dim || nconfig.dim; + var p = node.pos.getc(), pos = p.scale(node._scale); + var prod = nconfig.transform? nodeDim * (1 - p.squaredNorm()) : nodeDim; + if (prod >= nodeDim / 4) { + var c1x = pos.x, + c1y = pos.y - prod, + c2x = c1x - prod, + c2y = pos.y + prod, + c3x = c1x + prod, + c3y = c2y; + canvas.path('fill', function(ctx){ + ctx.moveTo(c1x, c1y); + ctx.lineTo(c2x, c2y); + ctx.lineTo(c3x, c3y); + }); + } + }, + + 'star': function(node, canvas) { + var nconfig = this.node, data = node.data; + var nodeDim = nconfig.overridable && data && data.$dim || nconfig.dim; + var p = node.pos.getc(), pos = p.scale(node._scale); + var prod = nconfig.transform? nodeDim * (1 - p.squaredNorm()) : nodeDim; + if (prod >= nodeDim / 4) { + var ctx = canvas.getCtx(), pi5 = Math.PI / 5; + ctx.save(); + ctx.translate(pos.x, pos.y); + ctx.beginPath(); + ctx.moveTo(nodeDim, 0); + for (var i = 0; i < 9; i++) { + ctx.rotate(pi5); + if (i % 2 == 0) { + ctx.lineTo((prod / 0.525731) * 0.200811, 0); + } + else { + ctx.lineTo(prod, 0); + } + } + ctx.closePath(); + ctx.fill(); + ctx.restore(); + } + } +}); + + /* + Class: Hypertree.Plot.EdgeTypes + + Here are implemented all kinds of edge rendering functions. + Rendering functions implemented are 'none', 'line' and 'hyperline'. + + You can add new Edge types by implementing a new method in this class + + Example: + + (start code js) + Hypertree.Plot.EdgeTypes.implement({ + 'newedgetypename': function(adj, canvas) { + //Render my edge here. + } + }); + (end code) + +*/ +Hypertree.Plot.EdgeTypes = new Class({ + 'none': function() {}, + + 'line': function(adj, canvas) { + var s = adj.nodeFrom._scale; + var pos = adj.nodeFrom.pos.getc(true); + var posChild = adj.nodeTo.pos.getc(true); + canvas.path('stroke', function(context) { + context.moveTo(pos.x * s, pos.y * s); + context.lineTo(posChild.x * s, posChild.y * s); + }); + }, + + 'hyperline': function(adj, canvas) { + this.hyperline(adj, canvas); + } +}); + + +/* + * File: Treemap.js + * + * Implements the class and other derived classes. + * + * Description: + * + * A Treemap is an information visualization technique, proven very useful when displaying large hierarchical structures on a constrained space. The idea behind a Treemap is to describe hierarchical relations as 'containment'. That means that if node B is child of node A, then B 'is contained' in A. + * + * Inspired by: + * + * Squarified Treemaps (Mark Bruls, Kees Huizing, and Jarke J. van Wijk) + * + * + * + * Tree visualization with tree-maps: 2-d space-filling approach (Ben Shneiderman) + * + * + * + * Disclaimer: + * + * This visualization was built from scratch, taking only these papers as inspiration, and only shares some features with the Treemap papers mentioned above. + * + */ + +/* + Object: TM + + Abstract Treemap object. + + Implemented By: + + , and . + + Description: + + Implements layout and configuration options inherited by , and . + + All Treemap constructors take the same configuration object as parameter. + + Two special _data_ keys are read from the JSON tree structure loaded by to calculate + node's color and dimensions. These properties are $area (for nodes dimensions) and $color. Both of these properties are floats. + + This means that the tree structure defined in should now look more like this + + (start code js) + var json = { + "id": "aUniqueIdentifier", + "name": "usually a nodes name", + "data": { + "$area": 33, //some float value + "$color": 36, //-optional- some float value + "some key": "some value", + "some other key": "some other value" + }, + "children": [ 'other nodes or empty' ] + }; + (end code) + + If you want to know more about JSON tree structures and the _data_ property please read . + + Configuration: + + *General* + + - _rootId_ The id of the div container where the Treemap will be injected. Default's 'infovis'. + - _orientation_ For and only. The layout algorithm orientation. Possible values are 'h' or 'v'. + - _levelsToShow_ Max depth of the plotted tree. Useful when using the request method. + - _addLeftClickHandler_ Add a left click event handler to zoom in the Treemap view when clicking a node. Default's *false*. + - _addRightClickHandler_ Add a right click event handler to zoom out the Treemap view. Default's *false*. + - _selectPathOnHover_ If setted to *true* all nodes contained in the path between the hovered node and the root node will have an *in-path* CSS class. Default's *false*. + + *Nodes* + + There are two kinds of Treemap nodes. + + (see treemapnode.png) + + Inner nodes are nodes having children, like _Pearl Jam_. + + These nodes are represented by three div elements. A _content_ element, a _head_ element (where the title goes) and a _body_ element, where the children are laid out. + + (start code xml) +
+
Pearl Jam
+
...other nodes here...
+
+ (end code) + + Leaves are optionally colored nodes laying at the "bottom" of the tree. For example, _Yield_, _Vs._ and _Riot Act_ are leaves. + + These nodes are represented by two div elements. A _content_ element and a wrapped _leaf_ element + + (start code xml) +
+
Yield
+
+ (end code) + + There are some configuration properties regarding Treemap nodes + + - _titleHeight_ The height of the title (_head_) div container. Default's 13. + - _offset_ The separation offset between the _content_ div element and its contained div(s). Default's 4. + + *Color* + + _Color_ is an object containing as properties + + - _allow_ If *true*, the algorithm will check for the JSON node data _$color_ property to add some color to the Treemap leaves. + This color is calculated by interpolating a node's $color value range with a real RGB color range. + By specifying min|maxValues for the $color property and min|maxColorValues for the RGB counterparts, the visualization is able to + interpolate color values and assign a proper color to the leaf node. Default's *false*. + - _minValue_ The minimum value expected for the $color value property. Used for interpolating. Default's -100. + - _maxValue_ The maximum value expected for the $color value property. Used for interpolating. Default's 100. + - _minColorValue_ A three-element RGB array defining the color to be assigned to the _$color_ having _minValue_ as value. Default's [255, 0, 50]. + - _maxColorValue_ A three-element RGB array defining the color to be assigned to the _$color_ having _maxValue_ as value. Default's [0, 255, 50]. + + *Tips* + + _Tips_ is an object containing as properties + + - _allow_ If *true*, a tooltip will be shown when a node is hovered. The tooltip is a div DOM element having "tip" as CSS class. Default's *false*. + - _offsetX_ An offset added to the current tooltip x-position (which is the same as the current mouse position). Default's 20. + - _offsetY_ An offset added to the current tooltip y-position (which is the same as the current mouse position). Default's 20. + - _onShow(tooltip, node, isLeaf, domElement)_ Implement this method to change the HTML content of the tooltip when hovering a node. + + Parameters: + tooltip - The tooltip div element. + node - The corresponding JSON tree node (See also ). + isLeaf - Whether is a leaf or inner node. + domElement - The current hovered DOM element. + + *Controller options* + + You can also implement controller functions inside the configuration object. These functions are + + - _onBeforeCompute(node)_ This method is called right before performing all computation and animations to the JIT visualization. + - _onAfterCompute()_ This method is triggered right after all animations or computations for the JIT visualizations ended. + - _onCreateElement(content, node, isLeaf, elem1, elem2)_ This method is called on each newly created node. + + Parameters: + content - The div wrapper element with _content_ className. + node - The corresponding JSON tree node (See also ). + isLeaf - Whether is a leaf or inner node. If the node's an inner tree node, elem1 and elem2 will become the _head_ and _body_ div elements respectively. + If the node's a _leaf_, then elem1 will become the div leaf element. + + - _onDestroyElement(content, node, isLeaf, elem1, elem2)_ This method is called before collecting each node. Takes the same parameters as onCreateElement. + - _request(nodeId, level, onComplete)_ This method is used for buffering information into the visualization. When clicking on an empty node, + the visualization will make a request for this node's subtrees, specifying a given level for this subtree (defined by _levelsToShow_). Once the request is completed, the _onComplete_ +object should be called with the given result. + + See also , and . + + +*/ +this.TM = { + + layout: { + orientation: "h", + vertical: function() { + return this.orientation == "v"; + }, + horizontal: function() { + return this.orientation == "h"; + }, + change: function() { + this.orientation = this.vertical()? "h" : "v"; + } + }, + + innerController: { + onBeforeCompute: $empty, + onAfterCompute: $empty, + onComplete: $empty, + onCreateElement: $empty, + onDestroyElement: $empty, + request: false + }, + + config: { + orientation: "h", + titleHeight: 13, + rootId: 'infovis', + offset:4, + levelsToShow: 3, + addLeftClickHandler: false, + addRightClickHandler: false, + selectPathOnHover: false, + + Color: { + allow: false, + minValue: -100, + maxValue: 100, + minColorValue: [255, 0, 50], + maxColorValue: [0, 255, 50] + }, + + Tips: { + allow: false, + offsetX: 20, + offsetY: 20, + onShow: $empty + } + }, + + + initialize: function(controller) { + this.tree = null; + this.shownTree = null; + this.controller = this.config = $merge(this.config, + this.innerController, + controller); + this.rootId = this.config.rootId; + this.layout.orientation = this.config.orientation; + //add tooltip + if(this.config.Tips.allow && document.body) { + var tip = document.getElementById('_tooltip') || document.createElement('div'); + tip.id = '_tooltip'; + tip.className = 'tip'; + var style = tip.style; + style.position = 'absolute'; + style.display = 'none'; + style.zIndex = 13000; + document.body.appendChild(tip); + this.tip = tip; + } + + //purge + var that = this; + var fn = function() { + that.empty(); + if(window.CollectGarbage) window.CollectGarbage(); + delete fn; + }; + if(window.addEventListener) { + window.addEventListener('unload', fn, false); + } else { + window.attachEvent('onunload', fn); + } + }, + + /* + Method: each + + Traverses head and leaf nodes applying a given function + + Parameters: + + f - A function that takes as parameters the same as the onCreateElement and onDestroyElement methods described in . + */ + each: function(f) { + (function rec(elem) { + if(!elem) return; + var ch = elem.childNodes, len = ch.length; + if(len > 0) { + f.apply(this, [elem, len === 1, ch[0], ch[1]]); + } + if (len > 1) { + for(var chi = ch[1].childNodes, i=0; i + + Returns: + + A boolean value specifying if the node is a tree leaf or not. + + */ + leaf: function(tree) { + return tree.children == 0; + }, + + /* + Method: createBox + + Constructs the proper DOM layout from a json node. + + If the node's an _inner node_, + this method calls , and + to create the following HTML structure + + (start code xml) +
+
[Node name]
+
[Node's children]
+
+ (end code) + + If the node's a leaf node, it creates the following structure + by calling , + + (start code xml) +
+
[Node name]
+
+ (end code) + + + Parameters: + + json - A JSON subtree. See also . + coord - A coordinates object specifying width, height, left and top style properties. + html - html to inject into the _body_ element if the node is an inner Tree node. + + Returns: + + The HTML structure described above. + + See also: + + , , , , . + + */ + createBox: function(json, coord, html) { + var box; + if(!this.leaf(json)) { + box = this.headBox(json, coord) + this.bodyBox(html, coord); + } else { + box = this.leafBox(json, coord); + } + return this.contentBox(json, coord, box); + }, + + /* + Method: plot + + Renders the Treemap. + + Parameters: + + json - A JSON tree structure preprocessed by some Treemap layout algorithm. + + Returns: + + The HTML to inject to the main visualization container. + + See also: + + . + + + */ + plot: function(json) { + var coord = json.coord, html = ""; + + if(this.leaf(json)) + return this.createBox(json, coord, null); + + for(var i=0, ch=json.children; i 1) { + html+= this.plot(chi); + } + } + return this.createBox(json, coord, html); + }, + + + /* + Method: headBox + + Creates the _head_ div dom element that usually contains the name of a parent JSON tree node. + + Parameters: + + json - A JSON subtree. See also . + coord - width and height base coordinate object. + + Returns: + + A new _head_ div dom element that has _head_ as class name. + + See also: + + . + + */ + headBox: function(json, coord) { + var config = this.config, offst = config.offset; + var c = { + 'height': config.titleHeight + "px", + 'width': (coord.width - offst) + "px", + 'left': offst / 2 + "px" + }; + return "
" + + json.name + "
"; + }, + + /* + Method: bodyBox + + Creates the _body_ div dom element that usually contains a subtree dom element layout. + + Parameters: + + html - html that should be contained in the body html. + coord - width and height base coordinate object. + + Returns: + + A new _body_ div dom element that has _body_ as class name. + + See also: + + . + + */ + bodyBox: function(html, coord) { + var config = this.config, + th = config.titleHeight, + offst = config.offset; + var c = { + 'width': (coord.width - offst) + "px", + 'height':(coord.height - offst - th) + "px", + 'top': (th + offst / 2) + "px", + 'left': (offst / 2) + "px" + }; + return "
" + html + "
"; + }, + + + + /* + Method: contentBox + + Creates the _content_ div dom element that usually contains a _leaf_ div dom element or _head_ and _body_ div dom elements. + + Parameters: + + json - A JSON node. See also . + coord - An object containing width, height, left and top coordinates. + html - input html wrapped by this tag. + + Returns: + + A new _content_ div dom element that has _content_ as class name. + + See also: + + . + + */ + contentBox: function(json, coord, html) { + var c = {}; + for(var i in coord) c[i] = coord[i] + "px"; + return "
" + html + "
"; + }, + + + /* + Method: leafBox + + Creates the _leaf_ div dom element that usually contains nothing else. + + Parameters: + + json - A JSON subtree. See also . + coord - base with and height coordinate object. + + Returns: + + A new _leaf_ div dom element having _leaf_ as class name. + + See also: + + . + + + */ + leafBox: function(json, coord) { + var config = this.config; + var backgroundColor = config.Color.allow && this.setColor(json), + offst = config.offset, + width = coord.width - offst, + height = coord.height - offst; + var c = { + 'top': (offst / 2) + "px", + 'height':height + "px", + 'width': width + "px", + 'left': (offst / 2) + "px" + }; + if(backgroundColor) c['background-color'] = backgroundColor; + return "
" + + json.name + "
"; + }, + + + /* + Method: setColor + + Calculates an hexa color string based on the _$color_ data node property. + + This method is called by to assign an hexadecimal color to each leaf node. + + This color is calculated by making a linear interpolation between _$color_ max and min values and + RGB max and min values so that + + > hex = (maxColorValue - minColorValue) / (maxValue - minValue) * (x - minValue) + minColorValue + + where _x_ range is [minValue, maxValue] and + + - _minValue_ + - _maxValue_ + - _minColorValue_ + - _maxColorValue_ + + are defined in the configuration object. + + This method is called by iif _Color.allow_ is setted to _true_. + + Sometimes linear interpolation for coloring is just not enough. In that case you can re-implement this + method so that it fits your coloring needs. + + Some people might find useful to implement their own coloring interpolation method and to assign the resulting hex string + to the _$color_ property. In that case we could re-implement the method like this + + (start code js) + //TM.Strip, TM.SliceAndDice also work + TM.Squarified.implement({ + 'setColor': function(json) { + return json.data.$color; + } + }); + (end code) + + So that it returns the previously assigned hex string. + + Parameters: + + json - A JSON tree node. + + Returns: + + A String that represents a color in hex value. + + */ + setColor: function(json) { + var c = this.config.Color, + maxcv = c.maxColorValue, + mincv = c.minColorValue, + maxv = c.maxValue, + minv = c.minValue, + diff = maxv - minv, + x = (json.data.$color - 0); + //linear interpolation + var comp = function(i, x) { + return Math.round((((maxcv[i] - mincv[i]) / diff) * (x - minv) + mincv[i])); + }; + + return $rgbToHex([ comp(0, x), comp(1, x), comp(2, x) ]); + }, + + /* + Method: enter + + Sets the _elem_ parameter as root and performs the layout. + + Parameters: + + elem - A JSON Tree node. See also . + */ + enter: function(elem) { + this.view(elem.parentNode.id); + }, + + /* + Method: onLeftClick + + Sets the _elem_ parameter as root and performs the layout. + This method is called when _addLeftClickHandler_ is *true* and a + node is left-clicked. You can override this method to add some custom behavior + when the node is left clicked though. + + An Example for overriding this method could be + (start code js) + //TM.Strip or TM.SliceAndDice also work + TM.Squarified.implement({ + 'onLeftClick': function(elem) { + //some custom code... + } + }); + (end code) + + + Parameters: + + elem - A JSON Tree node. See also . + + See also: + + + */ + onLeftClick: function(elem) { + this.enter(elem); + }, + + /* + Method: out + + Sets the _parent_ node of the currently shown subtree as root and performs the layout. + + */ + out: function() { + var parent = TreeUtil.getParent(this.tree, this.shownTree.id); + if(parent) { + if(this.controller.request) + TreeUtil.prune(parent, this.config.levelsToShow); + this.view(parent.id); + } + }, + + /* + Method: onRightClick + + Sets the _parent_ node of the currently shown subtree as root and performs the layout. + This method is called when _addRightClickHandler_ is *true* and a + node is right-clicked. You can override this method to add some custom behavior + when the node is right-clicked though. + + An Example for overriding this method could be + (start code js) + //TM.Strip or TM.SliceAndDice also work + TM.Squarified.implement({ + 'onRightClick': function() { + //some custom code... + } + }); + (end code) + + See also: + + + + */ + onRightClick: function() { + this.out(); + }, + + /* + Method: view + + Sets the root of the treemap to the specified node id and performs the layout. + + Parameters: + + id - A node identifier + */ + view: function(id) { + var config = this.config, that = this; + var post = { + onComplete: function() { + that.loadTree(id); + $get(config.rootId).focus(); + } + }; + + if (this.controller.request) { + var TUtil = TreeUtil; + TUtil.loadSubtrees(TUtil.getSubtree(this.tree, id), + $merge(this.controller, post)); + } else { + post.onComplete(); + } + }, + + /* + Method: resetPath + + Sets an 'in-path' className for _leaf_ and _head_ elements which belong to the path between the given tree node + and the visualization's root node. + + Parameters: + + tree - A JSON tree node. See also . + */ + resetPath: function(tree) { + var root = this.rootId, previous = this.resetPath.previous; + this.resetPath.previous = tree || false; + function getParent(c) { + var p = c.parentNode; + return p && (p.id != root) && p; + }; + function toggleInPath(elem, remove) { + if(elem) { + var container = $get(elem.id); + if(container) { + var parent = getParent(container); + while(parent) { + elem = parent.childNodes[0]; + if($hasClass(elem, 'in-path')) { + if(remove == undefined || !!remove) $removeClass(elem, 'in-path'); + } else { + if(!remove) $addClass(elem, 'in-path'); + } + parent = getParent(parent); + } + } + } + }; + toggleInPath(previous, true); + toggleInPath(tree, false); + }, + + + /* + Method: initializeElements + + Traverses the DOM tree applying the onCreateElement method. + + The onCreateElement controller method should attach events and add some behavior to the DOM element + node created. *By default, the Treemap wont add any event to its elements.* + */ + initializeElements: function() { + var cont = this.controller, that = this; + var ff = $lambda(false), tipsAllow = cont.Tips.allow; + this.each(function(content, isLeaf, elem1, elem2) { + var tree = TreeUtil.getSubtree(that.tree, content.id); + cont.onCreateElement(content, tree, isLeaf, elem1, elem2); + + //eliminate context menu when right clicking + if(cont.addRightClickHandler) elem1.oncontextmenu = ff; + + //add click handlers + if(cont.addLeftClickHandler || cont.addRightClickHandler) { + $addEvent(elem1, 'mouseup', function(e) { + var rightClick = (e.which == 3 || e.button == 2); + if (rightClick) { + if(cont.addRightClickHandler) that.onRightClick(); + } + else { + if(cont.addLeftClickHandler) that.onLeftClick(elem1); + } + + //prevent default + if (e.preventDefault) + e.preventDefault(); + else + e.returnValue = false; + }); + } + + //add path selection on hovering nodes + if(cont.selectPathOnHover || tipsAllow) { + $addEvent(elem1, 'mouseover', function(e){ + if(cont.selectPathOnHover) { + if (isLeaf) { + $addClass(elem1, 'over-leaf'); + } + else { + $addClass(elem1, 'over-head'); + $addClass(content, 'over-content'); + } + if (content.id) + that.resetPath(tree); + } + if(tipsAllow) + cont.Tips.onShow(that.tip, tree, isLeaf, elem1); + }); + + $addEvent(elem1, 'mouseout', function(e){ + if(cont.selectPathOnHover) { + if (isLeaf) { + $removeClass(elem1, 'over-leaf'); + } + else { + $removeClass(elem1, 'over-head'); + $removeClass(content, 'over-content'); + } + that.resetPath(); + } + if(tipsAllow) + that.tip.style.display = 'none'; + }); + + if(tipsAllow) { + //Add mousemove event handler + $addEvent(elem1, 'mousemove', function(e, win){ + var tip = that.tip; + //get mouse position + win = win || window; + e = e || win.event; + var doc = win.document; + doc = doc.html || doc.body; + var page = { + x: e.pageX || e.clientX + doc.scrollLeft, + y: e.pageY || e.clientY + doc.scrollTop + }; + tip.style.display = ''; + //get window dimensions + win = { + 'height': document.body.clientHeight, + 'width': document.body.clientWidth + }; + //get tooltip dimensions + var obj = { + 'width': tip.offsetWidth, + 'height': tip.offsetHeight + }; + //set tooltip position + var style = tip.style, x = cont.Tips.offsetX, y = cont.Tips.offsetY; + style.top = ((page.y + y + obj.height > win.height)? + (page.y - obj.height - y) : page.y + y) + 'px'; + style.left = ((page.x + obj.width + x > win.width)? + (page.x - obj.width - x) : page.x + x) + 'px'; + }); + } + } + }); + }, + + /* + Method: destroyElements + + Traverses the tree applying the onDestroyElement method. + + The onDestroyElement controller method should detach events and garbage collect the element. + *By default, the Treemap adds some garbage collect facilities for IE.* + */ + destroyElements: function() { + if(this.controller.onDestroyElement != $empty) { + var cont = this.controller, that = this; + this.each(function(content, isLeaf, elem1, elem2) { + cont.onDestroyElement(content, TreeUtil.getSubtree(that.tree, content.id), isLeaf, elem1, elem2); + }); + } + }, + + /* + Method: empty + + Empties the Treemap container (trying also to garbage collect things). + */ + empty: function() { + this.destroyElements(); + $clean($get(this.rootId)); + }, + + /* + Method: loadTree + + Loads the subtree specified by _id_ and plots it on the layout container. + + Parameters: + + id - A subtree id. + */ + loadTree: function(id) { + this.empty(); + this.loadJSON(TreeUtil.getSubtree(this.tree, id)); + } + +}; + +/* + Class: TM.SliceAndDice + + A JavaScript implementation of the Slice and Dice Treemap algorithm. + + The constructor takes an _optional_ configuration object described in . + + This visualization (as all other Treemap visualizations) is fed with JSON Tree structures. + + The _$area_ node data key is required for calculating rectangles dimensions. + + The _$color_ node data key is required if _Color_ _allow_ is *true* and is used for calculating + leaves colors. + + Extends: + + + Parameters: + + config - Configuration defined in . + + Example: + + + Here's a way of instanciating the will all its _optional_ configuration features + + (start code js) + + var tm = new TM.SliceAndDice({ + orientation: "h", + titleHeight: 13, + rootId: 'infovis', + offset:4, + levelsToShow: 3, + addLeftClickHandler: false, + addRightClickHandler: false, + selectPathOnHover: false, + + Color: { + allow: false, + minValue: -100, + maxValue: 100, + minColorValue: [255, 0, 50], + maxColorValue: [0, 255, 50] + }, + + Tips: { + allow: false, + offsetX; 20, + offsetY: 20, + onShow: function(tooltip, node, isLeaf, domElement) {} + }, + + onBeforeCompute: function(node) { + //Some stuff on before compute... + }, + onAfterCompute: function() { + //Some stuff on after compute... + }, + onCreateElement: function(content, node, isLeaf, head, body) { + //Some stuff onCreateElement + }, + onDestroyElement: function(content, node, isLeaf, head, body) { + //Some stuff onDestroyElement + }, + request: false + }); + tm.loadJSON(json); + + (end code) + +*/ +TM.SliceAndDice = new Class({ + Implements: TM, + /* + Method: loadJSON + + Loads the specified JSON tree and lays it on the main container. + + Parameters: + + json - A JSON Tree. See also . + */ + loadJSON: function (json) { + this.controller.onBeforeCompute(json); + var container = $get(this.rootId), + config = this.config, + width = container.offsetWidth, + height = container.offsetHeight; + + var p = { + 'coord': { + 'top': 0, + 'left': 0, + 'width': width, + 'height': height + config.titleHeight + config.offset + } + }; + + if(this.tree == null) this.tree = json; + this.shownTree = json; + this.compute(p, json, this.layout.orientation); + container.innerHTML = this.plot(json); + this.initializeElements(); + this.controller.onAfterCompute(json); + }, + + /* + Method: compute + + Called by loadJSON to calculate recursively all node positions and lay out the tree. + + Parameters: + + par - The parent node of the json subtree. + json - A JSON subtree. See also . + orientation - The current orientation. This value is switched recursively. + */ + compute: function(par, json, orientation) { + var config = this.config, + coord = par.coord, + offst = config.offset, + width = coord.width - offst, + height = coord.height - offst - config.titleHeight, + pdata = par.data, + fact = (pdata && ("$area" in pdata))? json.data.$area / pdata.$area : 1; + var otherSize, size, dim, pos, pos2; + + var horizontal = (orientation == "h"); + if(horizontal) { + orientation = 'v'; + otherSize = height; + size = Math.round(width * fact); + dim = 'height'; + pos = 'top'; + pos2 = 'left'; + } else { + orientation = 'h'; + otherSize = Math.round(height * fact); + size = width; + dim = 'width'; + pos = 'left'; + pos2 = 'top'; + } + json.coord = { + 'width':size, + 'height':otherSize, + 'top':0, + 'left':0 + }; + var offsetSize = 0, tm = this; + $each(json.children, function(elem){ + tm.compute(json, elem, orientation); + elem.coord[pos] = offsetSize; + elem.coord[pos2] = 0; + offsetSize += Math.floor(elem.coord[dim]); + }); + } +}); + + +/* + Class: TM.Area + + Abstract Treemap class containing methods that are common to + aspect ratio related algorithms such as and . + + Implemented by: + + , +*/ +TM.Area = new Class({ + + /* + Method: loadJSON + + Loads the specified JSON tree and lays it on the main container. + + Parameters: + + json - A JSON tree. See also . + */ + loadJSON: function (json) { + this.controller.onBeforeCompute(json); + var container = $get(this.rootId), + width = container.offsetWidth, + height = container.offsetHeight, + offst = this.config.offset, + offwdth = width - offst, + offhght = height - offst - this.config.titleHeight; + + json.coord = { + 'height': height, + 'width': width, + 'top': 0, + 'left': 0 + }; + var coord = $merge(json.coord, { + 'width': offwdth, + 'height': offhght + }); + + this.compute(json, coord); + container.innerHTML = this.plot(json); + if(this.tree == null) this.tree = json; + this.shownTree = json; + this.initializeElements(); + this.controller.onAfterCompute(json); + }, + + /* + Method: computeDim + + Computes dimensions and positions of a group of nodes + according to a custom layout row condition. + + Parameters: + + tail - An array of nodes. + initElem - An array of nodes (containing the initial node to be laid). + w - A fixed dimension where nodes will be layed out. + coord - A coordinates object specifying width, height, left and top style properties. + comp - A custom comparison function + */ + computeDim: function(tail, initElem, w, coord, comp) { + if(tail.length + initElem.length == 1) { + var l = (tail.length == 1)? tail : initElem; + this.layoutLast(l, w, coord); + return; + } + if(tail.length >= 2 && initElem.length == 0) { + initElem = [tail[0]]; + tail = tail.slice(1); + } + if(tail.length == 0) { + if(initElem.length > 0) this.layoutRow(initElem, w, coord); + return; + } + var c = tail[0]; + if(comp(initElem, w) >= comp([c].concat(initElem), w)) { + this.computeDim(tail.slice(1), initElem.concat([c]), w, coord, comp); + } else { + var newCoords = this.layoutRow(initElem, w, coord); + this.computeDim(tail, [], newCoords.dim, newCoords, comp); + } + }, + + + /* + Method: worstAspectRatio + + Calculates the worst aspect ratio of a group of rectangles. + + See also: + + + + Parameters: + + ch - An array of nodes. + w - The fixed dimension where rectangles are being laid out. + + Returns: + + The worst aspect ratio. + + + */ + worstAspectRatio: function(ch, w) { + if(!ch || ch.length == 0) return Number.MAX_VALUE; + var areaSum = 0, maxArea = 0, minArea = Number.MAX_VALUE; + for(var i=0; i area)? maxArea : area; + } + var sqw = w * w, sqAreaSum = areaSum * areaSum; + return Math.max(sqw * maxArea / sqAreaSum, + sqAreaSum / (sqw * minArea)); + }, + + /* + Method: avgAspectRatio + + Calculates the average aspect ratio of a group of rectangles. + + See also: + + + + Parameters: + + ch - An array of nodes. + w - The fixed dimension where rectangles are being laid out. + + Returns: + + The average aspect ratio. + + + */ + avgAspectRatio: function(ch, w) { + if(!ch || ch.length == 0) return Number.MAX_VALUE; + var arSum = 0; + for(var i=0; i h)? w / h : h / w; + } + return arSum / ch.length; + }, + + /* + layoutLast + + Performs the layout of the last computed sibling. + + Parameters: + + ch - An array of nodes. + w - A fixed dimension where nodes will be layed out. + coord - A coordinates object specifying width, height, left and top style properties. + */ + layoutLast: function(ch, w, coord) { + ch[0].coord = coord; + } + +}); + + + + +/* + Class: TM.Squarified + + A JavaScript implementation of the Squarified Treemap algorithm. + + The constructor takes an _optional_ configuration object described in . + + This visualization (as all other Treemap visualizations) is fed with JSON Tree structures. + + The _$area_ node data key is required for calculating rectangles dimensions. + + The _$color_ node data key is required if _Color_ _allow_ is *true* and is used for calculating + leaves colors. + + Extends: + and + + Parameters: + + config - Configuration defined in . + + Example: + + + Here's a way of instanciating the will all its _optional_ configuration features + + (start code js) + + var tm = new TM.Squarified({ + titleHeight: 13, + rootId: 'infovis', + offset:4, + levelsToShow: 3, + addLeftClickHandler: false, + addRightClickHandler: false, + selectPathOnHover: false, + + Color: { + allow: false, + minValue: -100, + maxValue: 100, + minColorValue: [255, 0, 50], + maxColorValue: [0, 255, 50] + }, + + Tips: { + allow: false, + offsetX: 20, + offsetY: 20, + onShow: function(tooltip, node, isLeaf, domElement) {} + }, + + onBeforeCompute: function(node) { + //Some stuff on before compute... + }, + onAfterCompute: function() { + //Some stuff on after compute... + }, + onCreateElement: function(content, node, isLeaf, head, body) { + //Some stuff onCreateElement + }, + onDestroyElement: function(content, node, isLeaf, head, body) { + //Some stuff onDestroyElement + }, + request: false + }); + + tm.loadJSON(json); + + (end code) + +*/ + +TM.Squarified = new Class({ + Implements: [TM, TM.Area], + + /* + Method: compute + + Called by loadJSON to calculate recursively all node positions and lay out the tree. + + Parameters: + + json - A JSON tree. See also . + coord - A coordinates object specifying width, height, left and top style properties. + */ + compute: function(json, coord) { + if (!(coord.width >= coord.height && this.layout.horizontal())) + this.layout.change(); + var ch = json.children, config = this.config; + if(ch.length > 0) { + this.processChildrenLayout(json, ch, coord); + for(var i=0; i= b._area); }); + var initElem = [ch[0]]; + var tail = ch.slice(1); + this.squarify(tail, initElem, minimumSideValue, coord); + }, + + /* + Method: squarify + + Performs an heuristic method to calculate div elements sizes in order to have a good aspect ratio. + + Parameters: + + tail - An array of nodes. + initElem - An array of nodes, containing the initial node to be laid out. + w - A fixed dimension where nodes will be laid out. + coord - A coordinates object specifying width, height, left and top style properties. + */ + squarify: function(tail, initElem, w, coord) { + this.computeDim(tail, initElem, w, coord, this.worstAspectRatio); + }, + + /* + Method: layoutRow + + Performs the layout of an array of nodes. + + Parameters: + + ch - An array of nodes. + w - A fixed dimension where nodes will be laid out. + coord - A coordinates object specifying width, height, left and top style properties. + */ + layoutRow: function(ch, w, coord) { + if(this.layout.horizontal()) { + return this.layoutV(ch, w, coord); + } else { + return this.layoutH(ch, w, coord); + } + }, + + layoutV: function(ch, w, coord) { + var totalArea = 0, rnd = Math.round; + $each(ch, function(elem) { totalArea += elem._area; }); + var width = rnd(totalArea / w), top = 0; + for(var i=0; i constructor takes an _optional_ configuration object described in . + + This visualization (as all other Treemap visualizations) is fed with JSON Tree structures. + + The _$area_ node data key is required for calculating rectangles dimensions. + + The _$color_ node data key is required if _Color_ _allow_ is *true* and is used for calculating + leaves colors. + + Extends: + and + + Parameters: + + config - Configuration defined in . + + Example: + + + Here's a way of instanciating the will all its _optional_ configuration features + + (start code js) + + var tm = new TM.Strip({ + titleHeight: 13, + orientation: "h", + rootId: 'infovis', + offset:4, + levelsToShow: 3, + addLeftClickHandler: false, + addRightClickHandler: false, + selectPathOnHover: false, + + Color: { + allow: false, + minValue: -100, + maxValue: 100, + minColorValue: [255, 0, 50], + maxColorValue: [0, 255, 50] + }, + + Tips: { + allow: false, + offsetX: 20, + offsetY: 20, + onShow: function(tooltip, node, isLeaf, domElement) {} + }, + + onBeforeCompute: function(node) { + //Some stuff on before compute... + }, + onAfterCompute: function() { + //Some stuff on after compute... + }, + onCreateElement: function(content, node, isLeaf, head, body) { + //Some stuff onCreateElement + }, + onDestroyElement: function(content, node, isLeaf, head, body) { + //Some stuff onDestroyElement + }, + request: false + }); + tm.loadJSON(json); + + (end code) + +*/ + +TM.Strip = new Class({ + Implements: [ TM, TM.Area ], + + /* + Method: compute + + Called by loadJSON to calculate recursively all node positions and lay out the tree. + + Parameters: + + json - A JSON subtree. See also . + coord - A coordinates object specifying width, height, left and top style properties. + */ + compute: function(json, coord) { + var ch = json.children, config = this.config; + if(ch.length > 0) { + this.processChildrenLayout(json, ch, coord); + for(var i=0; i string + labelBoxBorderColor: "#ccc", // border color for the little label boxes + container: null, // container (as jQuery object) to put legend in, null means default on top of graph + position: "ne", // position of default legend container within plot + margin: 5, // distance from grid edge to default legend container within plot + backgroundColor: null, // null means auto-detect + backgroundOpacity: 0.85 // set to 0 to avoid background + }, + xaxis: { + mode: null, // null or "time" + min: null, // min. value to show, null means set automatically + max: null, // max. value to show, null means set automatically + autoscaleMargin: null, // margin in % to add if auto-setting min/max + ticks: null, // either [1, 3] or [[1, "a"], 3] or (fn: axis info -> ticks) or app. number of ticks for auto-ticks + tickFormatter: null, // fn: number -> string + labelWidth: null, // size of tick labels in pixels + labelHeight: null, + + // mode specific options + tickDecimals: null, // no. of decimals, null means auto + tickSize: null, // number or [number, "unit"] + minTickSize: null, // number or [number, "unit"] + monthNames: null, // list of names of months + timeformat: null // format string to use + }, + yaxis: { + autoscaleMargin: 0.02 + }, + x2axis: { + autoscaleMargin: null + }, + y2axis: { + autoscaleMargin: 0.02 + }, + points: { + show: false, + radius: 3, + lineWidth: 2, // in pixels + fill: true, + fillColor: "#ffffff" + }, + lines: { + show: false, + lineWidth: 2, // in pixels + fill: false, + fillColor: null + }, + bars: { + show: false, + lineWidth: 2, // in pixels + barWidth: 1, // in units of the x axis + fill: true, + fillColor: null, + align: "left" // or "center" + }, + grid: { + color: "#545454", // primary color used for outline and labels + backgroundColor: null, // null for transparent, else color + tickColor: "#dddddd", // color used for the ticks + labelMargin: 5, // in pixels + borderWidth: 2, + markings: null, // array of ranges or fn: axes -> array of ranges + markingsColor: "#f4f4f4", + markingsLineWidth: 2, + // interactive stuff + clickable: false, + hoverable: false, + autoHighlight: true, // highlight in case mouse is near + mouseActiveRadius: 10 // how far the mouse can be away to activate an item + }, + selection: { + mode: null, // one of null, "x", "y" or "xy" + color: "#e8cfac" + }, + shadowSize: 4 + }, + canvas = null, // the canvas for the plot itself + overlay = null, // canvas for interactive stuff on top of plot + eventHolder = null, // jQuery object that events should be bound to + ctx = null, octx = null, + target = target_, + axes = { xaxis: {}, yaxis: {}, x2axis: {}, y2axis: {} }, + plotOffset = { left: 0, right: 0, top: 0, bottom: 0}, + canvasWidth = 0, canvasHeight = 0, + plotWidth = 0, plotHeight = 0, + // dedicated to storing data for buggy standard compliance cases + workarounds = {}; + + this.setData = setData; + this.setupGrid = setupGrid; + this.draw = draw; + this.clearSelection = clearSelection; + this.setSelection = setSelection; + this.getCanvas = function() { return canvas; }; + this.getPlotOffset = function() { return plotOffset; }; + this.getData = function() { return series; }; + this.getAxes = function() { return axes; }; + this.highlight = highlight; + this.unhighlight = unhighlight; + + // initialize + parseOptions(options_); + setData(data_); + constructCanvas(); + setupGrid(); + draw(); + + + function setData(d) { + series = parseData(d); + + fillInSeriesOptions(); + processData(); + } + + function parseData(d) { + var res = []; + for (var i = 0; i < d.length; ++i) { + var s; + if (d[i].data) { + s = {}; + for (var v in d[i]) + s[v] = d[i][v]; + } + else { + s = { data: d[i] }; + } + res.push(s); + } + + return res; + } + + function parseOptions(o) { + $.extend(true, options, o); + + // backwards compatibility, to be removed in future + if (options.xaxis.noTicks && options.xaxis.ticks == null) + options.xaxis.ticks = options.xaxis.noTicks; + if (options.yaxis.noTicks && options.yaxis.ticks == null) + options.yaxis.ticks = options.yaxis.noTicks; + if (options.grid.coloredAreas) + options.grid.markings = options.grid.coloredAreas; + if (options.grid.coloredAreasColor) + options.grid.markingsColor = options.grid.coloredAreasColor; + } + + function fillInSeriesOptions() { + var i; + + // collect what we already got of colors + var neededColors = series.length, + usedColors = [], + assignedColors = []; + for (i = 0; i < series.length; ++i) { + var sc = series[i].color; + if (sc != null) { + --neededColors; + if (typeof sc == "number") + assignedColors.push(sc); + else + usedColors.push(parseColor(series[i].color)); + } + } + + // we might need to generate more colors if higher indices + // are assigned + for (i = 0; i < assignedColors.length; ++i) { + neededColors = Math.max(neededColors, assignedColors[i] + 1); + } + + // produce colors as needed + var colors = [], variation = 0; + i = 0; + while (colors.length < neededColors) { + var c; + if (options.colors.length == i) // check degenerate case + c = new Color(100, 100, 100); + else + c = parseColor(options.colors[i]); + + // vary color if needed + var sign = variation % 2 == 1 ? -1 : 1; + var factor = 1 + sign * Math.ceil(variation / 2) * 0.2; + c.scale(factor, factor, factor); + + // FIXME: if we're getting to close to something else, + // we should probably skip this one + colors.push(c); + + ++i; + if (i >= options.colors.length) { + i = 0; + ++variation; + } + } + + // fill in the options + var colori = 0, s; + for (i = 0; i < series.length; ++i) { + s = series[i]; + + // assign colors + if (s.color == null) { + s.color = colors[colori].toString(); + ++colori; + } + else if (typeof s.color == "number") + s.color = colors[s.color].toString(); + + // copy the rest + s.lines = $.extend(true, {}, options.lines, s.lines); + s.points = $.extend(true, {}, options.points, s.points); + s.bars = $.extend(true, {}, options.bars, s.bars); + if (s.shadowSize == null) + s.shadowSize = options.shadowSize; + if (s.xaxis && s.xaxis == 2) + s.xaxis = axes.x2axis; + else + s.xaxis = axes.xaxis; + if (s.yaxis && s.yaxis == 2) + s.yaxis = axes.y2axis; + else + s.yaxis = axes.yaxis; + } + } + + function processData() { + var topSentry = Number.POSITIVE_INFINITY, + bottomSentry = Number.NEGATIVE_INFINITY, + axis; + + for (axis in axes) { + axes[axis].datamin = topSentry; + axes[axis].datamax = bottomSentry; + axes[axis].used = false; + } + + for (var i = 0; i < series.length; ++i) { + var data = series[i].data, + axisx = series[i].xaxis, + axisy = series[i].yaxis, + mindelta = 0, maxdelta = 0; + + // make sure we got room for the bar + if (series[i].bars.show) { + mindelta = series[i].bars.align == "left" ? 0 : -series[i].bars.barWidth/2; + maxdelta = mindelta + series[i].bars.barWidth; + } + + axisx.used = axisy.used = true; + for (var j = 0; j < data.length; ++j) { + if (data[j] == null) + continue; + + var x = data[j][0], y = data[j][1]; + + // convert to number + if (x != null && !isNaN(x = +x)) { + if (x + mindelta < axisx.datamin) + axisx.datamin = x + mindelta; + if (x + maxdelta > axisx.datamax) + axisx.datamax = x + maxdelta; + } + + if (y != null && !isNaN(y = +y)) { + if (y < axisy.datamin) + axisy.datamin = y; + if (y > axisy.datamax) + axisy.datamax = y; + } + + if (x == null || y == null || isNaN(x) || isNaN(y)) + data[j] = null; // mark this point as invalid + } + } + + for (axis in axes) { + if (axes[axis].datamin == topSentry) + axes[axis].datamin = 0; + if (axes[axis].datamax == bottomSentry) + axes[axis].datamax = 1; + } + } + + function constructCanvas() { + canvasWidth = target.width(); + canvasHeight = target.height(); + target.html(""); // clear target + target.css("position", "relative"); // for positioning labels and overlay + + if (canvasWidth <= 0 || canvasHeight <= 0) + throw "Invalid dimensions for plot, width = " + canvasWidth + ", height = " + canvasHeight; + + // the canvas + canvas = $('').appendTo(target).get(0); + if ($.browser.msie) // excanvas hack + canvas = window.G_vmlCanvasManager.initElement(canvas); + ctx = canvas.getContext("2d"); + + // overlay canvas for interactive features + overlay = $('').appendTo(target).get(0); + if ($.browser.msie) // excanvas hack + overlay = window.G_vmlCanvasManager.initElement(overlay); + octx = overlay.getContext("2d"); + + // we include the canvas in the event holder too, because IE 7 + // sometimes has trouble with the stacking order + eventHolder = $([overlay, canvas]); + + // bind events + if (options.selection.mode != null || options.grid.hoverable) { + // FIXME: temp. work-around until jQuery bug 1871 is fixed + eventHolder.each(function () { + this.onmousemove = onMouseMove; + }); + + if (options.selection.mode != null) + eventHolder.mousedown(onMouseDown); + } + + if (options.grid.clickable) + eventHolder.click(onClick); + } + + function setupGrid() { + function setupAxis(axis, options) { + setRange(axis, options); + prepareTickGeneration(axis, options); + setTicks(axis, options); + // add transformation helpers + if (axis == axes.xaxis || axis == axes.x2axis) { + // data point to canvas coordinate + axis.p2c = function (p) { return (p - axis.min) * axis.scale; }; + // canvas coordinate to data point + axis.c2p = function (c) { return axis.min + c / axis.scale; }; + } + else { + axis.p2c = function (p) { return (axis.max - p) * axis.scale; }; + axis.c2p = function (p) { return axis.max - p / axis.scale; }; + } + } + + for (var axis in axes) + setupAxis(axes[axis], options[axis]); + + setSpacing(); + insertLabels(); + insertLegend(); + } + + function setRange(axis, axisOptions) { + var min = axisOptions.min != null ? axisOptions.min : axis.datamin; + var max = axisOptions.max != null ? axisOptions.max : axis.datamax; + + if (max - min == 0.0) { + // degenerate case + var widen; + if (max == 0.0) + widen = 1.0; + else + widen = 0.01; + + min -= widen; + max += widen; + } + else { + // consider autoscaling + var margin = axisOptions.autoscaleMargin; + if (margin != null) { + if (axisOptions.min == null) { + min -= (max - min) * margin; + // make sure we don't go below zero if all values + // are positive + if (min < 0 && axis.datamin >= 0) + min = 0; + } + if (axisOptions.max == null) { + max += (max - min) * margin; + if (max > 0 && axis.datamax <= 0) + max = 0; + } + } + } + axis.min = min; + axis.max = max; + } + + function prepareTickGeneration(axis, axisOptions) { + // estimate number of ticks + var noTicks; + if (typeof axisOptions.ticks == "number" && axisOptions.ticks > 0) + noTicks = axisOptions.ticks; + else if (axis == axes.xaxis || axis == axes.x2axis) + noTicks = canvasWidth / 100; + else + noTicks = canvasHeight / 60; + + var delta = (axis.max - axis.min) / noTicks; + var size, generator, unit, formatter, i, magn, norm; + + if (axisOptions.mode == "time") { + // pretty handling of time + + function formatDate(d, fmt, monthNames) { + var leftPad = function(n) { + n = "" + n; + return n.length == 1 ? "0" + n : n; + }; + + var r = []; + var escape = false; + if (monthNames == null) + monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; + for (var i = 0; i < fmt.length; ++i) { + var c = fmt.charAt(i); + + if (escape) { + switch (c) { + case 'h': c = "" + d.getUTCHours(); break; + case 'H': c = leftPad(d.getUTCHours()); break; + case 'M': c = leftPad(d.getUTCMinutes()); break; + case 'S': c = leftPad(d.getUTCSeconds()); break; + case 'd': c = "" + d.getUTCDate(); break; + case 'm': c = "" + (d.getUTCMonth() + 1); break; + case 'y': c = "" + d.getUTCFullYear(); break; + case 'b': c = "" + monthNames[d.getUTCMonth()]; break; + } + r.push(c); + escape = false; + } + else { + if (c == "%") + escape = true; + else + r.push(c); + } + } + return r.join(""); + } + + + // map of app. size of time units in milliseconds + var timeUnitSize = { + "second": 1000, + "minute": 60 * 1000, + "hour": 60 * 60 * 1000, + "day": 24 * 60 * 60 * 1000, + "month": 30 * 24 * 60 * 60 * 1000, + "year": 365.2425 * 24 * 60 * 60 * 1000 + }; + + + // the allowed tick sizes, after 1 year we use + // an integer algorithm + var spec = [ + [1, "second"], [2, "second"], [5, "second"], [10, "second"], + [30, "second"], + [1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"], + [30, "minute"], + [1, "hour"], [2, "hour"], [4, "hour"], + [8, "hour"], [12, "hour"], + [1, "day"], [2, "day"], [3, "day"], + [0.25, "month"], [0.5, "month"], [1, "month"], + [2, "month"], [3, "month"], [6, "month"], + [1, "year"] + ]; + + var minSize = 0; + if (axisOptions.minTickSize != null) { + if (typeof axisOptions.tickSize == "number") + minSize = axisOptions.tickSize; + else + minSize = axisOptions.minTickSize[0] * timeUnitSize[axisOptions.minTickSize[1]]; + } + + for (i = 0; i < spec.length - 1; ++i) + if (delta < (spec[i][0] * timeUnitSize[spec[i][1]] + + spec[i + 1][0] * timeUnitSize[spec[i + 1][1]]) / 2 + && spec[i][0] * timeUnitSize[spec[i][1]] >= minSize) + break; + size = spec[i][0]; + unit = spec[i][1]; + + // special-case the possibility of several years + if (unit == "year") { + magn = Math.pow(10, Math.floor(Math.log(delta / timeUnitSize.year) / Math.LN10)); + norm = (delta / timeUnitSize.year) / magn; + if (norm < 1.5) + size = 1; + else if (norm < 3) + size = 2; + else if (norm < 7.5) + size = 5; + else + size = 10; + + size *= magn; + } + + if (axisOptions.tickSize) { + size = axisOptions.tickSize[0]; + unit = axisOptions.tickSize[1]; + } + + generator = function(axis) { + var ticks = [], + tickSize = axis.tickSize[0], unit = axis.tickSize[1], + d = new Date(axis.min); + + var step = tickSize * timeUnitSize[unit]; + + if (unit == "second") + d.setUTCSeconds(floorInBase(d.getUTCSeconds(), tickSize)); + if (unit == "minute") + d.setUTCMinutes(floorInBase(d.getUTCMinutes(), tickSize)); + if (unit == "hour") + d.setUTCHours(floorInBase(d.getUTCHours(), tickSize)); + if (unit == "month") + d.setUTCMonth(floorInBase(d.getUTCMonth(), tickSize)); + if (unit == "year") + d.setUTCFullYear(floorInBase(d.getUTCFullYear(), tickSize)); + + // reset smaller components + d.setUTCMilliseconds(0); + if (step >= timeUnitSize.minute) + d.setUTCSeconds(0); + if (step >= timeUnitSize.hour) + d.setUTCMinutes(0); + if (step >= timeUnitSize.day) + d.setUTCHours(0); + if (step >= timeUnitSize.day * 4) + d.setUTCDate(1); + if (step >= timeUnitSize.year) + d.setUTCMonth(0); + + + var carry = 0, v = Number.NaN, prev; + do { + prev = v; + v = d.getTime(); + ticks.push({ v: v, label: axis.tickFormatter(v, axis) }); + if (unit == "month") { + if (tickSize < 1) { + // a bit complicated - we'll divide the month + // up but we need to take care of fractions + // so we don't end up in the middle of a day + d.setUTCDate(1); + var start = d.getTime(); + d.setUTCMonth(d.getUTCMonth() + 1); + var end = d.getTime(); + d.setTime(v + carry * timeUnitSize.hour + (end - start) * tickSize); + carry = d.getUTCHours(); + d.setUTCHours(0); + } + else + d.setUTCMonth(d.getUTCMonth() + tickSize); + } + else if (unit == "year") { + d.setUTCFullYear(d.getUTCFullYear() + tickSize); + } + else + d.setTime(v + step); + } while (v < axis.max && v != prev); + + return ticks; + }; + + formatter = function (v, axis) { + var d = new Date(v); + + // first check global format + if (axisOptions.timeformat != null) + return formatDate(d, axisOptions.timeformat, axisOptions.monthNames); + + var t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]]; + var span = axis.max - axis.min; + + if (t < timeUnitSize.minute) + fmt = "%h:%M:%S"; + else if (t < timeUnitSize.day) { + if (span < 2 * timeUnitSize.day) + fmt = "%h:%M"; + else + fmt = "%b %d %h:%M"; + } + else if (t < timeUnitSize.month) + fmt = "%b %d"; + else if (t < timeUnitSize.year) { + if (span < timeUnitSize.year) + fmt = "%b"; + else + fmt = "%b %y"; + } + else + fmt = "%y"; + + return formatDate(d, fmt, axisOptions.monthNames); + }; + } + else { + // pretty rounding of base-10 numbers + var maxDec = axisOptions.tickDecimals; + var dec = -Math.floor(Math.log(delta) / Math.LN10); + if (maxDec != null && dec > maxDec) + dec = maxDec; + + magn = Math.pow(10, -dec); + norm = delta / magn; // norm is between 1.0 and 10.0 + + if (norm < 1.5) + size = 1; + else if (norm < 3) { + size = 2; + // special case for 2.5, requires an extra decimal + if (norm > 2.25 && (maxDec == null || dec + 1 <= maxDec)) { + size = 2.5; + ++dec; + } + } + else if (norm < 7.5) + size = 5; + else + size = 10; + + size *= magn; + + if (axisOptions.minTickSize != null && size < axisOptions.minTickSize) + size = axisOptions.minTickSize; + + if (axisOptions.tickSize != null) + size = axisOptions.tickSize; + + axis.tickDecimals = Math.max(0, (maxDec != null) ? maxDec : dec); + + generator = function (axis) { + var ticks = []; + + // spew out all possible ticks + var start = floorInBase(axis.min, axis.tickSize), + i = 0, v = Number.NaN, prev; + do { + prev = v; + v = start + i * axis.tickSize; + ticks.push({ v: v, label: axis.tickFormatter(v, axis) }); + ++i; + } while (v < axis.max && v != prev); + return ticks; + }; + + formatter = function (v, axis) { + return v.toFixed(axis.tickDecimals); + }; + } + + axis.tickSize = unit ? [size, unit] : size; + axis.tickGenerator = generator; + if ($.isFunction(axisOptions.tickFormatter)) + axis.tickFormatter = function (v, axis) { return "" + axisOptions.tickFormatter(v, axis); }; + else + axis.tickFormatter = formatter; + if (axisOptions.labelWidth != null) + axis.labelWidth = axisOptions.labelWidth; + if (axisOptions.labelHeight != null) + axis.labelHeight = axisOptions.labelHeight; + } + + function setTicks(axis, axisOptions) { + axis.ticks = []; + + if (!axis.used) + return; + + if (axisOptions.ticks == null) + axis.ticks = axis.tickGenerator(axis); + else if (typeof axisOptions.ticks == "number") { + if (axisOptions.ticks > 0) + axis.ticks = axis.tickGenerator(axis); + } + else if (axisOptions.ticks) { + var ticks = axisOptions.ticks; + + if ($.isFunction(ticks)) + // generate the ticks + ticks = ticks({ min: axis.min, max: axis.max }); + + // clean up the user-supplied ticks, copy them over + var i, v; + for (i = 0; i < ticks.length; ++i) { + var label = null; + var t = ticks[i]; + if (typeof t == "object") { + v = t[0]; + if (t.length > 1) + label = t[1]; + } + else + v = t; + if (label == null) + label = axis.tickFormatter(v, axis); + axis.ticks[i] = { v: v, label: label }; + } + } + + if (axisOptions.autoscaleMargin != null && axis.ticks.length > 0) { + // snap to ticks + if (axisOptions.min == null) + axis.min = Math.min(axis.min, axis.ticks[0].v); + if (axisOptions.max == null && axis.ticks.length > 1) + axis.max = Math.min(axis.max, axis.ticks[axis.ticks.length - 1].v); + } + } + + function setSpacing() { + function measureXLabels(axis) { + // to avoid measuring the widths of the labels, we + // construct fixed-size boxes and put the labels inside + // them, we don't need the exact figures and the + // fixed-size box content is easy to center + if (axis.labelWidth == null) + axis.labelWidth = canvasWidth / 6; + + // measure x label heights + if (axis.labelHeight == null) { + labels = []; + for (i = 0; i < axis.ticks.length; ++i) { + l = axis.ticks[i].label; + if (l) + labels.push('
' + l + '
'); + } + + axis.labelHeight = 0; + if (labels.length > 0) { + var dummyDiv = $('
' + + labels.join("") + '
').appendTo(target); + axis.labelHeight = dummyDiv.height(); + dummyDiv.remove(); + } + } + } + + function measureYLabels(axis) { + if (axis.labelWidth == null || axis.labelHeight == null) { + var i, labels = [], l; + // calculate y label dimensions + for (i = 0; i < axis.ticks.length; ++i) { + l = axis.ticks[i].label; + if (l) + labels.push('
' + l + '
'); + } + + if (labels.length > 0) { + var dummyDiv = $('
' + + labels.join("") + '
').appendTo(target); + if (axis.labelWidth == null) + axis.labelWidth = dummyDiv.width(); + if (axis.labelHeight == null) + axis.labelHeight = dummyDiv.find("div").height(); + dummyDiv.remove(); + } + + if (axis.labelWidth == null) + axis.labelWidth = 0; + if (axis.labelHeight == null) + axis.labelHeight = 0; + } + } + + measureXLabels(axes.xaxis); + measureYLabels(axes.yaxis); + measureXLabels(axes.x2axis); + measureYLabels(axes.y2axis); + + // get the most space needed around the grid for things + // that may stick out + var maxOutset = options.grid.borderWidth / 2; + for (i = 0; i < series.length; ++i) + maxOutset = Math.max(maxOutset, 2 * (series[i].points.radius + series[i].points.lineWidth/2)); + + plotOffset.left = plotOffset.right = plotOffset.top = plotOffset.bottom = maxOutset; + + if (axes.xaxis.labelHeight > 0) + plotOffset.bottom = Math.max(maxOutset, axes.xaxis.labelHeight + options.grid.labelMargin); + if (axes.yaxis.labelWidth > 0) + plotOffset.left = Math.max(maxOutset, axes.yaxis.labelWidth + options.grid.labelMargin); + + if (axes.x2axis.labelHeight > 0) + plotOffset.top = Math.max(maxOutset, axes.x2axis.labelHeight + options.grid.labelMargin); + + if (axes.y2axis.labelWidth > 0) + plotOffset.right = Math.max(maxOutset, axes.y2axis.labelWidth + options.grid.labelMargin); + + plotWidth = canvasWidth - plotOffset.left - plotOffset.right; + plotHeight = canvasHeight - plotOffset.bottom - plotOffset.top; + + // precompute how much the axis is scaling a point in canvas space + axes.xaxis.scale = plotWidth / (axes.xaxis.max - axes.xaxis.min); + axes.yaxis.scale = plotHeight / (axes.yaxis.max - axes.yaxis.min); + axes.x2axis.scale = plotWidth / (axes.x2axis.max - axes.x2axis.min); + axes.y2axis.scale = plotHeight / (axes.y2axis.max - axes.y2axis.min); + } + + function draw() { + drawGrid(); + for (var i = 0; i < series.length; i++) { + drawSeries(series[i]); + } + } + + function extractRange(ranges, coord) { + var firstAxis = coord + "axis", + secondaryAxis = coord + "2axis", + axis, from, to, reverse; + + if (ranges[firstAxis]) { + axis = axes[firstAxis]; + from = ranges[firstAxis].from; + to = ranges[firstAxis].to; + } + else if (ranges[secondaryAxis]) { + axis = axes[secondaryAxis]; + from = ranges[secondaryAxis].from; + to = ranges[secondaryAxis].to; + } + else { + // backwards-compat stuff - to be removed in future + axis = axes[firstAxis]; + from = ranges[coord + "1"]; + to = ranges[coord + "2"]; + } + + // auto-reverse as an added bonus + if (from != null && to != null && from > to) + return { from: to, to: from, axis: axis }; + + return { from: from, to: to, axis: axis }; + } + + function drawGrid() { + var i; + + ctx.save(); + ctx.clearRect(0, 0, canvasWidth, canvasHeight); + ctx.translate(plotOffset.left, plotOffset.top); + + // draw background, if any + if (options.grid.backgroundColor) { + ctx.fillStyle = options.grid.backgroundColor; + ctx.fillRect(0, 0, plotWidth, plotHeight); + } + + // draw markings + if (options.grid.markings) { + var markings = options.grid.markings; + if ($.isFunction(markings)) + // xmin etc. are backwards-compatible, to be removed in future + markings = markings({ xmin: axes.xaxis.min, xmax: axes.xaxis.max, ymin: axes.yaxis.min, ymax: axes.yaxis.max, xaxis: axes.xaxis, yaxis: axes.yaxis, x2axis: axes.x2axis, y2axis: axes.y2axis }); + + for (i = 0; i < markings.length; ++i) { + var m = markings[i], + xrange = extractRange(m, "x"), + yrange = extractRange(m, "y"); + + // fill in missing + if (xrange.from == null) + xrange.from = xrange.axis.min; + if (xrange.to == null) + xrange.to = xrange.axis.max; + if (yrange.from == null) + yrange.from = yrange.axis.min; + if (yrange.to == null) + yrange.to = yrange.axis.max; + + // clip + if (xrange.to < xrange.axis.min || xrange.from > xrange.axis.max || + yrange.to < yrange.axis.min || yrange.from > yrange.axis.max) + continue; + + xrange.from = Math.max(xrange.from, xrange.axis.min); + xrange.to = Math.min(xrange.to, xrange.axis.max); + yrange.from = Math.max(yrange.from, yrange.axis.min); + yrange.to = Math.min(yrange.to, yrange.axis.max); + + if (xrange.from == xrange.to && yrange.from == yrange.to) + continue; + + // then draw + xrange.from = xrange.axis.p2c(xrange.from); + xrange.to = xrange.axis.p2c(xrange.to); + yrange.from = yrange.axis.p2c(yrange.from); + yrange.to = yrange.axis.p2c(yrange.to); + + if (xrange.from == xrange.to || yrange.from == yrange.to) { + // draw line + ctx.strokeStyle = m.color || options.grid.markingsColor; + ctx.lineWidth = m.lineWidth || options.grid.markingsLineWidth; + ctx.moveTo(Math.floor(xrange.from), Math.floor(yrange.from)); + ctx.lineTo(Math.floor(xrange.to), Math.floor(yrange.to)); + ctx.stroke(); + } + else { + // fill area + ctx.fillStyle = m.color || options.grid.markingsColor; + ctx.fillRect(Math.floor(xrange.from), + Math.floor(yrange.to), + Math.floor(xrange.to - xrange.from), + Math.floor(yrange.from - yrange.to)); + } + } + } + + // draw the inner grid + ctx.lineWidth = 1; + ctx.strokeStyle = options.grid.tickColor; + ctx.beginPath(); + var v, axis = axes.xaxis; + for (i = 0; i < axis.ticks.length; ++i) { + v = axis.ticks[i].v; + if (v <= axis.min || v >= axes.xaxis.max) + continue; // skip those lying on the axes + + ctx.moveTo(Math.floor(axis.p2c(v)) + ctx.lineWidth/2, 0); + ctx.lineTo(Math.floor(axis.p2c(v)) + ctx.lineWidth/2, plotHeight); + } + + axis = axes.yaxis; + for (i = 0; i < axis.ticks.length; ++i) { + v = axis.ticks[i].v; + if (v <= axis.min || v >= axis.max) + continue; + + ctx.moveTo(0, Math.floor(axis.p2c(v)) + ctx.lineWidth/2); + ctx.lineTo(plotWidth, Math.floor(axis.p2c(v)) + ctx.lineWidth/2); + } + + axis = axes.x2axis; + for (i = 0; i < axis.ticks.length; ++i) { + v = axis.ticks[i].v; + if (v <= axis.min || v >= axis.max) + continue; + + ctx.moveTo(Math.floor(axis.p2c(v)) + ctx.lineWidth/2, -5); + ctx.lineTo(Math.floor(axis.p2c(v)) + ctx.lineWidth/2, 5); + } + + axis = axes.y2axis; + for (i = 0; i < axis.ticks.length; ++i) { + v = axis.ticks[i].v; + if (v <= axis.min || v >= axis.max) + continue; + + ctx.moveTo(plotWidth-5, Math.floor(axis.p2c(v)) + ctx.lineWidth/2); + ctx.lineTo(plotWidth+5, Math.floor(axis.p2c(v)) + ctx.lineWidth/2); + } + + ctx.stroke(); + + if (options.grid.borderWidth) { + // draw border + ctx.lineWidth = options.grid.borderWidth; + ctx.strokeStyle = options.grid.color; + ctx.lineJoin = "round"; + ctx.strokeRect(0, 0, plotWidth, plotHeight); + } + + ctx.restore(); + } + + function insertLabels() { + target.find(".tickLabels").remove(); + + var html = '
'; + + function addLabels(axis, labelGenerator) { + for (var i = 0; i < axis.ticks.length; ++i) { + var tick = axis.ticks[i]; + if (!tick.label || tick.v < axis.min || tick.v > axis.max) + continue; + html += labelGenerator(tick, axis); + } + } + + addLabels(axes.xaxis, function (tick, axis) { + return '
' + tick.label + "
"; + }); + + + addLabels(axes.yaxis, function (tick, axis) { + return '
' + tick.label + "
"; + }); + + addLabels(axes.x2axis, function (tick, axis) { + return '
' + tick.label + "
"; + }); + + addLabels(axes.y2axis, function (tick, axis) { + return '
' + tick.label + "
"; + }); + + html += '
'; + + target.append(html); + } + + function drawSeries(series) { + if (series.lines.show || (!series.bars.show && !series.points.show)) + drawSeriesLines(series); + if (series.bars.show) + drawSeriesBars(series); + if (series.points.show) + drawSeriesPoints(series); + } + + function drawSeriesLines(series) { + function plotLine(data, offset, axisx, axisy) { + var prev, cur = null, drawx = null, drawy = null; + + ctx.beginPath(); + for (var i = 0; i < data.length; ++i) { + prev = cur; + cur = data[i]; + + if (prev == null || cur == null) + continue; + + var x1 = prev[0], y1 = prev[1], + x2 = cur[0], y2 = cur[1]; + + // clip with ymin + if (y1 <= y2 && y1 < axisy.min) { + if (y2 < axisy.min) + continue; // line segment is outside + // compute new intersection point + x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; + y1 = axisy.min; + } + else if (y2 <= y1 && y2 < axisy.min) { + if (y1 < axisy.min) + continue; + x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; + y2 = axisy.min; + } + + // clip with ymax + if (y1 >= y2 && y1 > axisy.max) { + if (y2 > axisy.max) + continue; + x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; + y1 = axisy.max; + } + else if (y2 >= y1 && y2 > axisy.max) { + if (y1 > axisy.max) + continue; + x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; + y2 = axisy.max; + } + + // clip with xmin + if (x1 <= x2 && x1 < axisx.min) { + if (x2 < axisx.min) + continue; + y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; + x1 = axisx.min; + } + else if (x2 <= x1 && x2 < axisx.min) { + if (x1 < axisx.min) + continue; + y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; + x2 = axisx.min; + } + + // clip with xmax + if (x1 >= x2 && x1 > axisx.max) { + if (x2 > axisx.max) + continue; + y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; + x1 = axisx.max; + } + else if (x2 >= x1 && x2 > axisx.max) { + if (x1 > axisx.max) + continue; + y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; + x2 = axisx.max; + } + + if (drawx != axisx.p2c(x1) || drawy != axisy.p2c(y1) + offset) + ctx.moveTo(axisx.p2c(x1), axisy.p2c(y1) + offset); + + drawx = axisx.p2c(x2); + drawy = axisy.p2c(y2) + offset; + ctx.lineTo(drawx, drawy); + } + ctx.stroke(); + } + + function plotLineArea(data, axisx, axisy) { + var prev, cur = null; + + var bottom = Math.min(Math.max(0, axisy.min), axisy.max); + var top, lastX = 0; + + var areaOpen = false; + + for (var i = 0; i < data.length; ++i) { + prev = cur; + cur = data[i]; + + if (areaOpen && prev != null && cur == null) { + // close area + ctx.lineTo(axisx.p2c(lastX), axisy.p2c(bottom)); + ctx.fill(); + areaOpen = false; + continue; + } + + if (prev == null || cur == null) + continue; + + var x1 = prev[0], y1 = prev[1], + x2 = cur[0], y2 = cur[1]; + + // clip x values + + // clip with xmin + if (x1 <= x2 && x1 < axisx.min) { + if (x2 < axisx.min) + continue; + y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; + x1 = axisx.min; + } + else if (x2 <= x1 && x2 < axisx.min) { + if (x1 < axisx.min) + continue; + y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; + x2 = axisx.min; + } + + // clip with xmax + if (x1 >= x2 && x1 > axisx.max) { + if (x2 > axisx.max) + continue; + y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; + x1 = axisx.max; + } + else if (x2 >= x1 && x2 > axisx.max) { + if (x1 > axisx.max) + continue; + y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; + x2 = axisx.max; + } + + if (!areaOpen) { + // open area + ctx.beginPath(); + ctx.moveTo(axisx.p2c(x1), axisy.p2c(bottom)); + areaOpen = true; + } + + // now first check the case where both is outside + if (y1 >= axisy.max && y2 >= axisy.max) { + ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.max)); + ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.max)); + continue; + } + else if (y1 <= axisy.min && y2 <= axisy.min) { + ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.min)); + ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.min)); + continue; + } + + // else it's a bit more complicated, there might + // be two rectangles and two triangles we need to fill + // in; to find these keep track of the current x values + var x1old = x1, x2old = x2; + + // and clip the y values, without shortcutting + + // clip with ymin + if (y1 <= y2 && y1 < axisy.min && y2 >= axisy.min) { + x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; + y1 = axisy.min; + } + else if (y2 <= y1 && y2 < axisy.min && y1 >= axisy.min) { + x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; + y2 = axisy.min; + } + + // clip with ymax + if (y1 >= y2 && y1 > axisy.max && y2 <= axisy.max) { + x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; + y1 = axisy.max; + } + else if (y2 >= y1 && y2 > axisy.max && y1 <= axisy.max) { + x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; + y2 = axisy.max; + } + + + // if the x value was changed we got a rectangle + // to fill + if (x1 != x1old) { + if (y1 <= axisy.min) + top = axisy.min; + else + top = axisy.max; + + ctx.lineTo(axisx.p2c(x1old), axisy.p2c(top)); + ctx.lineTo(axisx.p2c(x1), axisy.p2c(top)); + } + + // fill the triangles + ctx.lineTo(axisx.p2c(x1), axisy.p2c(y1)); + ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2)); + + // fill the other rectangle if it's there + if (x2 != x2old) { + if (y2 <= axisy.min) + top = axisy.min; + else + top = axisy.max; + + ctx.lineTo(axisx.p2c(x2old), axisy.p2c(top)); + ctx.lineTo(axisx.p2c(x2), axisy.p2c(top)); + } + + lastX = Math.max(x2, x2old); + } + + if (areaOpen) { + ctx.lineTo(axisx.p2c(lastX), axisy.p2c(bottom)); + ctx.fill(); + } + } + + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + ctx.lineJoin = "round"; + + var lw = series.lines.lineWidth; + var sw = series.shadowSize; + // FIXME: consider another form of shadow when filling is turned on + if (sw > 0) { + // draw shadow in two steps + ctx.lineWidth = sw / 2; + ctx.strokeStyle = "rgba(0,0,0,0.1)"; + plotLine(series.data, lw/2 + sw/2 + ctx.lineWidth/2, series.xaxis, series.yaxis); + + ctx.lineWidth = sw / 2; + ctx.strokeStyle = "rgba(0,0,0,0.2)"; + plotLine(series.data, lw/2 + ctx.lineWidth/2, series.xaxis, series.yaxis); + } + + ctx.lineWidth = lw; + ctx.strokeStyle = series.color; + setFillStyle(series.lines, series.color); + if (series.lines.fill) + plotLineArea(series.data, series.xaxis, series.yaxis); + plotLine(series.data, 0, series.xaxis, series.yaxis); + ctx.restore(); + } + + function drawSeriesPoints(series) { + function plotPoints(data, radius, fill, axisx, axisy) { + for (var i = 0; i < data.length; ++i) { + if (data[i] == null) + continue; + + var x = data[i][0], y = data[i][1]; + if (x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) + continue; + + ctx.beginPath(); + ctx.arc(axisx.p2c(x), axisy.p2c(y), radius, 0, 2 * Math.PI, true); + if (fill) + ctx.fill(); + ctx.stroke(); + } + } + + function plotPointShadows(data, offset, radius, axisx, axisy) { + for (var i = 0; i < data.length; ++i) { + if (data[i] == null) + continue; + + var x = data[i][0], y = data[i][1]; + if (x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) + continue; + ctx.beginPath(); + ctx.arc(axisx.p2c(x), axisy.p2c(y) + offset, radius, 0, Math.PI, false); + ctx.stroke(); + } + } + + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + + var lw = series.lines.lineWidth; + var sw = series.shadowSize; + if (sw > 0) { + // draw shadow in two steps + ctx.lineWidth = sw / 2; + ctx.strokeStyle = "rgba(0,0,0,0.1)"; + plotPointShadows(series.data, sw/2 + ctx.lineWidth/2, + series.points.radius, series.xaxis, series.yaxis); + + ctx.lineWidth = sw / 2; + ctx.strokeStyle = "rgba(0,0,0,0.2)"; + plotPointShadows(series.data, ctx.lineWidth/2, + series.points.radius, series.xaxis, series.yaxis); + } + + ctx.lineWidth = series.points.lineWidth; + ctx.strokeStyle = series.color; + setFillStyle(series.points, series.color); + plotPoints(series.data, series.points.radius, series.points.fill, + series.xaxis, series.yaxis); + ctx.restore(); + } + + function drawBar(x, y, barLeft, barRight, offset, fill, axisx, axisy, c) { + var drawLeft = true, drawRight = true, + drawTop = true, drawBottom = false, + left = x + barLeft, right = x + barRight, + bottom = 0, top = y; + + // account for negative bars + if (top < bottom) { + top = 0; + bottom = y; + drawBottom = true; + drawTop = false; + } + + // clip + if (right < axisx.min || left > axisx.max || + top < axisy.min || bottom > axisy.max) + return; + + if (left < axisx.min) { + left = axisx.min; + drawLeft = false; + } + + if (right > axisx.max) { + right = axisx.max; + drawRight = false; + } + + if (bottom < axisy.min) { + bottom = axisy.min; + drawBottom = false; + } + + if (top > axisy.max) { + top = axisy.max; + drawTop = false; + } + + // fill the bar + if (fill) { + c.beginPath(); + c.moveTo(axisx.p2c(left), axisy.p2c(bottom) + offset); + c.lineTo(axisx.p2c(left), axisy.p2c(top) + offset); + c.lineTo(axisx.p2c(right), axisy.p2c(top) + offset); + c.lineTo(axisx.p2c(right), axisy.p2c(bottom) + offset); + c.fill(); + } + + // draw outline + if (drawLeft || drawRight || drawTop || drawBottom) { + c.beginPath(); + left = axisx.p2c(left); + bottom = axisy.p2c(bottom); + right = axisx.p2c(right); + top = axisy.p2c(top); + + c.moveTo(left, bottom + offset); + if (drawLeft) + c.lineTo(left, top + offset); + else + c.moveTo(left, top + offset); + if (drawTop) + c.lineTo(right, top + offset); + else + c.moveTo(right, top + offset); + if (drawRight) + c.lineTo(right, bottom + offset); + else + c.moveTo(right, bottom + offset); + if (drawBottom) + c.lineTo(left, bottom + offset); + else + c.moveTo(left, bottom + offset); + c.stroke(); + } + } + + function drawSeriesBars(series) { + function plotBars(data, barLeft, barRight, offset, fill, axisx, axisy) { + for (var i = 0; i < data.length; i++) { + if (data[i] == null) + continue; + drawBar(data[i][0], data[i][1], barLeft, barRight, offset, fill, axisx, axisy, ctx); + } + } + + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + ctx.lineJoin = "round"; + + // FIXME: figure out a way to add shadows + /* + var bw = series.bars.barWidth; + var lw = series.bars.lineWidth; + var sw = series.shadowSize; + if (sw > 0) { + // draw shadow in two steps + ctx.lineWidth = sw / 2; + ctx.strokeStyle = "rgba(0,0,0,0.1)"; + plotBars(series.data, bw, lw/2 + sw/2 + ctx.lineWidth/2, false); + + ctx.lineWidth = sw / 2; + ctx.strokeStyle = "rgba(0,0,0,0.2)"; + plotBars(series.data, bw, lw/2 + ctx.lineWidth/2, false); + }*/ + + ctx.lineWidth = series.bars.lineWidth; + ctx.strokeStyle = series.color; + setFillStyle(series.bars, series.color); + var barLeft = series.bars.align == "left" ? 0 : -series.bars.barWidth/2; + plotBars(series.data, barLeft, barLeft + series.bars.barWidth, 0, series.bars.fill, series.xaxis, series.yaxis); + ctx.restore(); + } + + function setFillStyle(obj, seriesColor) { + var fill = obj.fill; + if (!fill) + return; + + if (obj.fillColor) + ctx.fillStyle = obj.fillColor; + else { + var c = parseColor(seriesColor); + c.a = typeof fill == "number" ? fill : 0.4; + c.normalize(); + ctx.fillStyle = c.toString(); + } + } + + function insertLegend() { + target.find(".legend").remove(); + + if (!options.legend.show) + return; + + var fragments = []; + var rowStarted = false; + for (i = 0; i < series.length; ++i) { + if (!series[i].label) + continue; + + if (i % options.legend.noColumns == 0) { + if (rowStarted) + fragments.push(''); + fragments.push(''); + rowStarted = true; + } + + var label = series[i].label; + if (options.legend.labelFormatter != null) + label = options.legend.labelFormatter(label); + + fragments.push( + '
' + + '' + label + ''); + } + if (rowStarted) + fragments.push(''); + + if (fragments.length == 0) + return; + + var table = '' + fragments.join("") + '
'; + if (options.legend.container != null) + options.legend.container.html(table); + else { + var pos = ""; + var p = options.legend.position, m = options.legend.margin; + if (p.charAt(0) == "n") + pos += 'top:' + (m + plotOffset.top) + 'px;'; + else if (p.charAt(0) == "s") + pos += 'bottom:' + (m + plotOffset.bottom) + 'px;'; + if (p.charAt(1) == "e") + pos += 'right:' + (m + plotOffset.right) + 'px;'; + else if (p.charAt(1) == "w") + pos += 'left:' + (m + plotOffset.left) + 'px;'; + var legend = $('
' + table.replace('style="', 'style="position:absolute;' + pos +';') + '
').appendTo(target); + if (options.legend.backgroundOpacity != 0.0) { + // put in the transparent background + // separately to avoid blended labels and + // label boxes + var c = options.legend.backgroundColor; + if (c == null) { + var tmp; + if (options.grid.backgroundColor) + tmp = options.grid.backgroundColor; + else + tmp = extractColor(legend); + c = parseColor(tmp).adjust(null, null, null, 1).toString(); + } + var div = legend.children(); + $('
').prependTo(legend).css('opacity', options.legend.backgroundOpacity); + + } + } + } + + + // interactive features + + var lastMousePos = { pageX: null, pageY: null }, + selection = { + first: { x: -1, y: -1}, second: { x: -1, y: -1}, + show: false, active: false }, + highlights = [], + clickIsMouseUp = false, + redrawTimeout = null, + hoverTimeout = null; + + // Returns the data item the mouse is over, or null if none is found + function findNearbyItem(mouseX, mouseY) { + var maxDistance = options.grid.mouseActiveRadius, + lowestDistance = maxDistance * maxDistance + 1, + item = null, foundPoint = false; + + function result(i, j) { + return { datapoint: series[i].data[j], + dataIndex: j, + series: series[i], + seriesIndex: i }; + } + + for (var i = 0; i < series.length; ++i) { + var data = series[i].data, + axisx = series[i].xaxis, + axisy = series[i].yaxis, + + // precompute some stuff to make the loop faster + mx = axisx.c2p(mouseX), + my = axisy.c2p(mouseY), + maxx = maxDistance / axisx.scale, + maxy = maxDistance / axisy.scale, + checkbar = series[i].bars.show, + checkpoint = !(series[i].bars.show && !(series[i].lines.show || series[i].points.show)), + barLeft = series[i].bars.align == "left" ? 0 : -series[i].bars.barWidth/2, + barRight = barLeft + series[i].bars.barWidth; + for (var j = 0; j < data.length; ++j) { + if (data[j] == null) + continue; + + var x = data[j][0], y = data[j][1]; + + if (checkbar) { + // For a bar graph, the cursor must be inside the bar + // and no other point can be nearby + if (!foundPoint && mx >= x + barLeft && + mx <= x + barRight && + my >= Math.min(0, y) && my <= Math.max(0, y)) + item = result(i, j); + } + + if (checkpoint) { + // For points and lines, the cursor must be within a + // certain distance to the data point + + // check bounding box first + if ((x - mx > maxx || x - mx < -maxx) || + (y - my > maxy || y - my < -maxy)) + continue; + + // We have to calculate distances in pixels, not in + // data units, because the scale of the axes may be different + var dx = Math.abs(axisx.p2c(x) - mouseX), + dy = Math.abs(axisy.p2c(y) - mouseY), + dist = dx * dx + dy * dy; + if (dist < lowestDistance) { + lowestDistance = dist; + foundPoint = true; + item = result(i, j); + } + } + } + } + + return item; + } + + function onMouseMove(ev) { + // FIXME: temp. work-around until jQuery bug 1871 is fixed + var e = ev || window.event; + if (e.pageX == null && e.clientX != null) { + var de = document.documentElement, b = document.body; + lastMousePos.pageX = e.clientX + (de && de.scrollLeft || b.scrollLeft || 0); + lastMousePos.pageY = e.clientY + (de && de.scrollTop || b.scrollTop || 0); + } + else { + lastMousePos.pageX = e.pageX; + lastMousePos.pageY = e.pageY; + } + + if (options.grid.hoverable && !hoverTimeout) + hoverTimeout = setTimeout(onHover, 100); + + if (selection.active) + updateSelection(lastMousePos); + } + + function onMouseDown(e) { + if (e.which != 1) // only accept left-click + return; + + // cancel out any text selections + document.body.focus(); + + // prevent text selection and drag in old-school browsers + if (document.onselectstart !== undefined && workarounds.onselectstart == null) { + workarounds.onselectstart = document.onselectstart; + document.onselectstart = function () { return false; }; + } + if (document.ondrag !== undefined && workarounds.ondrag == null) { + workarounds.ondrag = document.ondrag; + document.ondrag = function () { return false; }; + } + + setSelectionPos(selection.first, e); + + lastMousePos.pageX = null; + selection.active = true; + $(document).one("mouseup", onSelectionMouseUp); + } + + function onClick(e) { + if (clickIsMouseUp) { + clickIsMouseUp = false; + return; + } + + triggerClickHoverEvent("plotclick", e); + } + + function onHover() { + triggerClickHoverEvent("plothover", lastMousePos); + hoverTimeout = null; + } + + // trigger click or hover event (they send the same parameters + // so we share their code) + function triggerClickHoverEvent(eventname, event) { + var offset = eventHolder.offset(), + pos = { pageX: event.pageX, pageY: event.pageY }, + canvasX = event.pageX - offset.left - plotOffset.left, + canvasY = event.pageY - offset.top - plotOffset.top; + + if (axes.xaxis.used) + pos.x = axes.xaxis.c2p(canvasX); + if (axes.yaxis.used) + pos.y = axes.yaxis.c2p(canvasY); + if (axes.x2axis.used) + pos.x2 = axes.x2axis.c2p(canvasX); + if (axes.y2axis.used) + pos.y2 = axes.y2axis.c2p(canvasY); + + var item = findNearbyItem(canvasX, canvasY); + + if (item) { + // fill in mouse pos for any listeners out there + item.pageX = parseInt(item.series.xaxis.p2c(item.datapoint[0]) + offset.left + plotOffset.left); + item.pageY = parseInt(item.series.yaxis.p2c(item.datapoint[1]) + offset.top + plotOffset.top); + + + } + + if (options.grid.autoHighlight) { + for (var i = 0; i < highlights.length; ++i) { + var h = highlights[i]; + if (h.auto && + !(item && h.series == item.series && h.point == item.datapoint)) + unhighlight(h.series, h.point); + } + + if (item) + highlight(item.series, item.datapoint, true); + } + + target.trigger(eventname, [ pos, item ]); + } + + function triggerRedrawOverlay() { + if (!redrawTimeout) + redrawTimeout = setTimeout(redrawOverlay, 50); + } + + function redrawOverlay() { + redrawTimeout = null; + + // redraw highlights + octx.save(); + octx.clearRect(0, 0, canvasWidth, canvasHeight); + octx.translate(plotOffset.left, plotOffset.top); + + var i, hi; + for (i = 0; i < highlights.length; ++i) { + hi = highlights[i]; + + if (hi.series.bars.show) + drawBarHighlight(hi.series, hi.point); + else + drawPointHighlight(hi.series, hi.point); + } + octx.restore(); + + // redraw selection + if (selection.show && selectionIsSane()) { + octx.strokeStyle = parseColor(options.selection.color).scale(null, null, null, 0.8).toString(); + octx.lineWidth = 1; + ctx.lineJoin = "round"; + octx.fillStyle = parseColor(options.selection.color).scale(null, null, null, 0.4).toString(); + + var x = Math.min(selection.first.x, selection.second.x), + y = Math.min(selection.first.y, selection.second.y), + w = Math.abs(selection.second.x - selection.first.x), + h = Math.abs(selection.second.y - selection.first.y); + + octx.fillRect(x + plotOffset.left, y + plotOffset.top, w, h); + octx.strokeRect(x + plotOffset.left, y + plotOffset.top, w, h); + } + } + + function highlight(s, point, auto) { + if (typeof s == "number") + s = series[s]; + + if (typeof point == "number") + point = s.data[point]; + + var i = indexOfHighlight(s, point); + if (i == -1) { + highlights.push({ series: s, point: point, auto: auto }); + + triggerRedrawOverlay(); + } + else if (!auto) + highlights[i].auto = false; + } + + function unhighlight(s, point) { + if (typeof s == "number") + s = series[s]; + + if (typeof point == "number") + point = s.data[point]; + + var i = indexOfHighlight(s, point); + if (i != -1) { + highlights.splice(i, 1); + + triggerRedrawOverlay(); + } + } + + function indexOfHighlight(s, p) { + for (var i = 0; i < highlights.length; ++i) { + var h = highlights[i]; + if (h.series == s && h.point[0] == p[0] + && h.point[1] == p[1]) + return i; + } + return -1; + } + + function drawPointHighlight(series, point) { + var x = point[0], y = point[1], + axisx = series.xaxis, axisy = series.yaxis; + + if (x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) + return; + + var pointRadius = series.points.radius + series.points.lineWidth / 2; + octx.lineWidth = pointRadius; + octx.strokeStyle = parseColor(series.color).scale(1, 1, 1, 0.5).toString(); + var radius = 1.5 * pointRadius; + octx.beginPath(); + octx.arc(axisx.p2c(x), axisy.p2c(y), radius, 0, 2 * Math.PI, true); + octx.stroke(); + } + + function drawBarHighlight(series, point) { + octx.lineJoin = "round"; + octx.lineWidth = series.bars.lineWidth; + octx.strokeStyle = parseColor(series.color).scale(1, 1, 1, 0.5).toString(); + octx.fillStyle = parseColor(series.color).scale(1, 1, 1, 0.5).toString(); + var barLeft = series.bars.align == "left" ? 0 : -series.bars.barWidth/2; + drawBar(point[0], point[1], barLeft, barLeft + series.bars.barWidth, + 0, true, series.xaxis, series.yaxis, octx); + } + + function triggerSelectedEvent() { + var x1 = Math.min(selection.first.x, selection.second.x), + x2 = Math.max(selection.first.x, selection.second.x), + y1 = Math.max(selection.first.y, selection.second.y), + y2 = Math.min(selection.first.y, selection.second.y); + + var r = {}; + if (axes.xaxis.used) + r.xaxis = { from: axes.xaxis.c2p(x1), to: axes.xaxis.c2p(x2) }; + if (axes.x2axis.used) + r.x2axis = { from: axes.x2axis.c2p(x1), to: axes.x2axis.c2p(x2) }; + if (axes.yaxis.used) + r.yaxis = { from: axes.yaxis.c2p(y1), to: axes.yaxis.c2p(y2) }; + if (axes.y2axis.used) + r.yaxis = { from: axes.y2axis.c2p(y1), to: axes.y2axis.c2p(y2) }; + + target.trigger("plotselected", [ r ]); + + // backwards-compat stuff, to be removed in future + if (axes.xaxis.used && axes.yaxis.used) + target.trigger("selected", [ { x1: r.xaxis.from, y1: r.yaxis.from, x2: r.xaxis.to, y2: r.yaxis.to } ]); + } + + function onSelectionMouseUp(e) { + // revert drag stuff for old-school browsers + if (document.onselectstart !== undefined) + document.onselectstart = workarounds.onselectstart; + if (document.ondrag !== undefined) + document.ondrag = workarounds.ondrag; + + // no more draggy-dee-drag + selection.active = false; + updateSelection(e); + + if (selectionIsSane()) { + triggerSelectedEvent(); + clickIsMouseUp = true; + } + + return false; + } + + function setSelectionPos(pos, e) { + var offset = eventHolder.offset(); + if (options.selection.mode == "y") { + if (pos == selection.first) + pos.x = 0; + else + pos.x = plotWidth; + } + else { + pos.x = e.pageX - offset.left - plotOffset.left; + pos.x = Math.min(Math.max(0, pos.x), plotWidth); + } + + if (options.selection.mode == "x") { + if (pos == selection.first) + pos.y = 0; + else + pos.y = plotHeight; + } + else { + pos.y = e.pageY - offset.top - plotOffset.top; + pos.y = Math.min(Math.max(0, pos.y), plotHeight); + } + } + + function updateSelection(pos) { + if (pos.pageX == null) + return; + + setSelectionPos(selection.second, pos); + if (selectionIsSane()) { + selection.show = true; + triggerRedrawOverlay(); + } + else + clearSelection(); + } + + function clearSelection() { + if (selection.show) { + selection.show = false; + triggerRedrawOverlay(); + } + } + + function setSelection(ranges, preventEvent) { + var range; + + if (options.selection.mode == "y") { + selection.first.x = 0; + selection.second.x = plotWidth; + } + else { + range = extractRange(ranges, "x"); + + selection.first.x = range.axis.p2c(range.from); + selection.second.x = range.axis.p2c(range.to); + } + + if (options.selection.mode == "x") { + selection.first.y = 0; + selection.second.y = plotHeight; + } + else { + range = extractRange(ranges, "y"); + + selection.first.y = range.axis.p2c(range.from); + selection.second.y = range.axis.p2c(range.to); + } + + selection.show = true; + triggerRedrawOverlay(); + if (!preventEvent) + triggerSelectedEvent(); + } + + function selectionIsSane() { + var minSize = 5; + return Math.abs(selection.second.x - selection.first.x) >= minSize && + Math.abs(selection.second.y - selection.first.y) >= minSize; + } + } + + $.plot = function(target, data, options) { + var plot = new Plot(target, data, options); + /*var t0 = new Date(); + var t1 = new Date(); + var tstr = "time used (msecs): " + (t1.getTime() - t0.getTime()) + if (window.console) + console.log(tstr); + else + alert(tstr);*/ + return plot; + }; + + // round to nearby lower multiple of base + function floorInBase(n, base) { + return base * Math.floor(n / base); + } + + function clamp(min, value, max) { + if (value < min) + return value; + else if (value > max) + return max; + else + return value; + } + + // color helpers, inspiration from the jquery color animation + // plugin by John Resig + function Color (r, g, b, a) { + + var rgba = ['r','g','b','a']; + var x = 4; //rgba.length + + while (-1<--x) { + this[rgba[x]] = arguments[x] || ((x==3) ? 1.0 : 0); + } + + this.toString = function() { + if (this.a >= 1.0) { + return "rgb("+[this.r,this.g,this.b].join(",")+")"; + } else { + return "rgba("+[this.r,this.g,this.b,this.a].join(",")+")"; + } + }; + + this.scale = function(rf, gf, bf, af) { + x = 4; //rgba.length + while (-1<--x) { + if (arguments[x] != null) + this[rgba[x]] *= arguments[x]; + } + return this.normalize(); + }; + + this.adjust = function(rd, gd, bd, ad) { + x = 4; //rgba.length + while (-1<--x) { + if (arguments[x] != null) + this[rgba[x]] += arguments[x]; + } + return this.normalize(); + }; + + this.clone = function() { + return new Color(this.r, this.b, this.g, this.a); + }; + + var limit = function(val,minVal,maxVal) { + return Math.max(Math.min(val, maxVal), minVal); + }; + + this.normalize = function() { + this.r = limit(parseInt(this.r), 0, 255); + this.g = limit(parseInt(this.g), 0, 255); + this.b = limit(parseInt(this.b), 0, 255); + this.a = limit(this.a, 0, 1); + return this; + }; + + this.normalize(); + } + + var lookupColors = { + aqua:[0,255,255], + azure:[240,255,255], + beige:[245,245,220], + black:[0,0,0], + blue:[0,0,255], + brown:[165,42,42], + cyan:[0,255,255], + darkblue:[0,0,139], + darkcyan:[0,139,139], + darkgrey:[169,169,169], + darkgreen:[0,100,0], + darkkhaki:[189,183,107], + darkmagenta:[139,0,139], + darkolivegreen:[85,107,47], + darkorange:[255,140,0], + darkorchid:[153,50,204], + darkred:[139,0,0], + darksalmon:[233,150,122], + darkviolet:[148,0,211], + fuchsia:[255,0,255], + gold:[255,215,0], + green:[0,128,0], + indigo:[75,0,130], + khaki:[240,230,140], + lightblue:[173,216,230], + lightcyan:[224,255,255], + lightgreen:[144,238,144], + lightgrey:[211,211,211], + lightpink:[255,182,193], + lightyellow:[255,255,224], + lime:[0,255,0], + magenta:[255,0,255], + maroon:[128,0,0], + navy:[0,0,128], + olive:[128,128,0], + orange:[255,165,0], + pink:[255,192,203], + purple:[128,0,128], + violet:[128,0,128], + red:[255,0,0], + silver:[192,192,192], + white:[255,255,255], + yellow:[255,255,0] + }; + + function extractColor(element) { + var color, elem = element; + do { + color = elem.css("background-color").toLowerCase(); + // keep going until we find an element that has color, or + // we hit the body + if (color != '' && color != 'transparent') + break; + elem = elem.parent(); + } while (!$.nodeName(elem.get(0), "body")); + + // catch Safari's way of signalling transparent + if (color == "rgba(0, 0, 0, 0)") + return "transparent"; + + return color; + } + + // parse string, returns Color + function parseColor(str) { + var result; + + // Look for rgb(num,num,num) + if (result = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(str)) + return new Color(parseInt(result[1], 10), parseInt(result[2], 10), parseInt(result[3], 10)); + + // Look for rgba(num,num,num,num) + if (result = /rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str)) + return new Color(parseInt(result[1], 10), parseInt(result[2], 10), parseInt(result[3], 10), parseFloat(result[4])); + + // Look for rgb(num%,num%,num%) + if (result = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(str)) + return new Color(parseFloat(result[1])*2.55, parseFloat(result[2])*2.55, parseFloat(result[3])*2.55); + + // Look for rgba(num%,num%,num%,num) + if (result = /rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str)) + return new Color(parseFloat(result[1])*2.55, parseFloat(result[2])*2.55, parseFloat(result[3])*2.55, parseFloat(result[4])); + + // Look for #a0b1c2 + if (result = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(str)) + return new Color(parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16)); + + // Look for #fff + if (result = /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(str)) + return new Color(parseInt(result[1]+result[1], 16), parseInt(result[2]+result[2], 16), parseInt(result[3]+result[3], 16)); + + // Otherwise, we're most likely dealing with a named color + var name = $.trim(str).toLowerCase(); + if (name == "transparent") + return new Color(255, 255, 255, 0); + else { + result = lookupColors[name]; + return new Color(result[0], result[1], result[2]); + } + } + +})(jQuery); \ No newline at end of file diff --git a/modules/distribution/src/repository/resources/dashboard/gadgets/js/jquery.form.js b/modules/distribution/src/repository/resources/dashboard/gadgets/js/jquery.form.js new file mode 100644 index 00000000..659baa98 --- /dev/null +++ b/modules/distribution/src/repository/resources/dashboard/gadgets/js/jquery.form.js @@ -0,0 +1,601 @@ +/* + * jQuery Form Plugin + * version: 2.12 (06/07/2008) + * @requires jQuery v1.2.2 or later + * + * Examples and documentation at: http://malsup.com/jquery/form/ + * Dual licensed under the MIT and GPL licenses: + * http://www.opensource.org/licenses/mit-license.php + * http://www.gnu.org/licenses/gpl.html + * + * Revision: $Id$ + */ +(function($) { + +/* + Usage Note: + ----------- + Do not use both ajaxSubmit and ajaxForm on the same form. These + functions are intended to be exclusive. Use ajaxSubmit if you want + to bind your own submit handler to the form. For example, + + $(document).ready(function() { + $('#myForm').bind('submit', function() { + $(this).ajaxSubmit({ + target: '#output' + }); + return false; // <-- important! + }); + }); + + Use ajaxForm when you want the plugin to manage all the event binding + for you. For example, + + $(document).ready(function() { + $('#myForm').ajaxForm({ + target: '#output' + }); + }); + + When using ajaxForm, the ajaxSubmit function will be invoked for you + at the appropriate time. +*/ + +/** + * ajaxSubmit() provides a mechanism for immediately submitting + * an HTML form using AJAX. + */ +$.fn.ajaxSubmit = function(options) { + // fast fail if nothing selected (http://dev.jquery.com/ticket/2752) + if (!this.length) { + log('ajaxSubmit: skipping submit process - no element selected'); + return this; + } + + if (typeof options == 'function') + options = { success: options }; + + options = $.extend({ + url: this.attr('action') || window.location.toString(), + type: this.attr('method') || 'GET' + }, options || {}); + + // hook for manipulating the form data before it is extracted; + // convenient for use with rich editors like tinyMCE or FCKEditor + var veto = {}; + this.trigger('form-pre-serialize', [this, options, veto]); + if (veto.veto) { + log('ajaxSubmit: submit vetoed via form-pre-serialize trigger'); + return this; + } + + var a = this.formToArray(options.semantic); + if (options.data) { + options.extraData = options.data; + for (var n in options.data) + a.push( { name: n, value: options.data[n] } ); + } + + // give pre-submit callback an opportunity to abort the submit + if (options.beforeSubmit && options.beforeSubmit(a, this, options) === false) { + log('ajaxSubmit: submit aborted via beforeSubmit callback'); + return this; + } + + // fire vetoable 'validate' event + this.trigger('form-submit-validate', [a, this, options, veto]); + if (veto.veto) { + log('ajaxSubmit: submit vetoed via form-submit-validate trigger'); + return this; + } + + var q = $.param(a); + + if (options.type.toUpperCase() == 'GET') { + options.url += (options.url.indexOf('?') >= 0 ? '&' : '?') + q; + options.data = null; // data is null for 'get' + } + else + options.data = q; // data is the query string for 'post' + + var $form = this, callbacks = []; + if (options.resetForm) callbacks.push(function() { $form.resetForm(); }); + if (options.clearForm) callbacks.push(function() { $form.clearForm(); }); + + // perform a load on the target only if dataType is not provided + if (!options.dataType && options.target) { + var oldSuccess = options.success || function(){}; + callbacks.push(function(data) { + $(options.target).html(data).each(oldSuccess, arguments); + }); + } + else if (options.success) + callbacks.push(options.success); + + options.success = function(data, status) { + for (var i=0, max=callbacks.length; i < max; i++) + callbacks[i](data, status, $form); + }; + + // are there files to upload? + var files = $('input:file', this).fieldValue(); + var found = false; + for (var j=0; j < files.length; j++) + if (files[j]) + found = true; + + // options.iframe allows user to force iframe mode + if (options.iframe || found) { + // hack to fix Safari hang (thanks to Tim Molendijk for this) + // see: http://groups.google.com/group/jquery-dev/browse_thread/thread/36395b7ab510dd5d + if ($.browser.safari && options.closeKeepAlive) + $.get(options.closeKeepAlive, fileUpload); + else + fileUpload(); + } + else + $.ajax(options); + + // fire 'notify' event + this.trigger('form-submit-notify', [this, options]); + return this; + + + // private function for handling file uploads (hat tip to YAHOO!) + function fileUpload() { + var form = $form[0]; + + if ($(':input[@name=submit]', form).length) { + alert('Error: Form elements must not be named "submit".'); + return; + } + + var opts = $.extend({}, $.ajaxSettings, options); + + var id = 'jqFormIO' + (new Date().getTime()); + var $io = $('